diff --git a/client/client.go b/client/client.go index 0a1d7632..db2d7c1d 100644 --- a/client/client.go +++ b/client/client.go @@ -33,7 +33,7 @@ import ( ) type Client struct { - client internalhttp.Client + Client internalhttp.Client } // New instantiates a new Client with default HTTP configuration @@ -49,282 +49,282 @@ func NewWithHttpClient(httpClient *http.Client, hostname, apiToken string) (*Cli return nil, err } return &Client{ - client: *client, + Client: *client, }, nil } func (c *Client) ReadAllConnectors(ctx context.Context, inp connector.ReadAllInput) (*connector.ReadAllOutput, error) { - return connector.ReadAll(ctx, c.client, inp) + return connector.ReadAll(ctx, c.Client, inp) } func (c *Client) ReadConnectorByName(ctx context.Context, inp connector.ReadByNameInput) (*connector.ReadOutput, error) { - return connector.ReadByName(ctx, c.client, inp) + return connector.ReadByName(ctx, c.Client, inp) } func (c *Client) ReadConnectorByUid(ctx context.Context, inp connector.ReadByUidInput) (*connector.ReadOutput, error) { - return connector.ReadByUid(ctx, c.client, inp) + return connector.ReadByUid(ctx, c.Client, inp) } func (c *Client) ReadAsa(ctx context.Context, inp asa.ReadInput) (*asa.ReadOutput, error) { - return asa.Read(ctx, c.client, inp) + return asa.Read(ctx, c.Client, inp) } func (c *Client) ReadDeviceByName(ctx context.Context, inp device.ReadByNameAndTypeInput) (*device.ReadOutput, error) { - return device.ReadByNameAndType(ctx, c.client, inp) + return device.ReadByNameAndType(ctx, c.Client, inp) } func (c *Client) CreateAsa(ctx context.Context, inp asa.CreateInput) (*asa.ReadOutput, *asa.ReadSpecificOutput, *asa.CreateError) { - return asa.Create(ctx, c.client, inp) + return asa.Create(ctx, c.Client, inp) } func (c *Client) UpdateAsa(ctx context.Context, inp asa.UpdateInput) (*asa.UpdateOutput, error) { - return asa.Update(ctx, c.client, inp) + return asa.Update(ctx, c.Client, inp) } func (c *Client) DeleteAsa(ctx context.Context, inp asa.DeleteInput) (*asa.DeleteOutput, error) { - return asa.Delete(ctx, c.client, inp) + return asa.Delete(ctx, c.Client, inp) } func (c *Client) ReadIos(ctx context.Context, inp ios.ReadInput) (*ios.ReadOutput, error) { - return ios.Read(ctx, c.client, inp) + return ios.Read(ctx, c.Client, inp) } func (c *Client) CreateIos(ctx context.Context, inp ios.CreateInput) (*ios.CreateOutput, error) { - return ios.Create(ctx, c.client, inp) + return ios.Create(ctx, c.Client, inp) } func (c *Client) UpdateIos(ctx context.Context, inp ios.UpdateInput) (*ios.UpdateOutput, error) { - return ios.Update(ctx, c.client, inp) + return ios.Update(ctx, c.Client, inp) } func (c *Client) DeleteIos(ctx context.Context, inp ios.DeleteInput) (*ios.DeleteOutput, error) { - return ios.Delete(ctx, c.client, inp) + return ios.Delete(ctx, c.Client, inp) } func (c *Client) ReadAsaConfig(ctx context.Context, inp asaconfig.ReadInput) (*asaconfig.ReadOutput, error) { - return asaconfig.Read(ctx, c.client, inp) + return asaconfig.Read(ctx, c.Client, inp) } func (c *Client) ReadSpecificAsa(ctx context.Context, inp asa.ReadSpecificInput) (*asa.ReadSpecificOutput, error) { - return asa.ReadSpecific(ctx, c.client, inp) + return asa.ReadSpecific(ctx, c.Client, inp) } func (c *Client) CreateConnector(ctx context.Context, inp connector.CreateInput) (*connector.CreateOutput, error) { - return connector.Create(ctx, c.client, inp) + return connector.Create(ctx, c.Client, inp) } func (c *Client) UpdateConnector(ctx context.Context, inp connector.UpdateInput) (*connector.UpdateOutput, error) { - return connector.Update(ctx, c.client, inp) + return connector.Update(ctx, c.Client, inp) } func (c *Client) DeleteConnector(ctx context.Context, inp connector.DeleteInput) (*connector.DeleteOutput, error) { - return connector.Delete(ctx, c.client, inp) + return connector.Delete(ctx, c.Client, inp) } func (c *Client) ReadGenericSSH(ctx context.Context, inp genericssh.ReadInput) (*genericssh.ReadOutput, error) { - return genericssh.Read(ctx, c.client, inp) + return genericssh.Read(ctx, c.Client, inp) } func (c *Client) CreateGenericSSH(ctx context.Context, inp genericssh.CreateInput) (*genericssh.CreateOutput, error) { - return genericssh.Create(ctx, c.client, inp) + return genericssh.Create(ctx, c.Client, inp) } func (c *Client) UpdateGenericSSH(ctx context.Context, inp genericssh.UpdateInput) (*genericssh.UpdateOutput, error) { - return genericssh.Update(ctx, c.client, inp) + return genericssh.Update(ctx, c.Client, inp) } func (c *Client) DeleteGenericSSH(ctx context.Context, inp genericssh.DeleteInput) (*genericssh.DeleteOutput, error) { - return genericssh.Delete(ctx, c.client, inp) + return genericssh.Delete(ctx, c.Client, inp) } func (c *Client) ReadCloudFtdByUid(ctx context.Context, inp cloudftd.ReadByUidInput) (*cloudftd.ReadOutput, error) { - return cloudftd.ReadByUid(ctx, c.client, inp) + return cloudftd.ReadByUid(ctx, c.Client, inp) } func (c *Client) ReadCloudFtdByName(ctx context.Context, inp cloudftd.ReadByNameInput) (*cloudftd.ReadOutput, error) { - return cloudftd.ReadByName(ctx, c.client, inp) + return cloudftd.ReadByName(ctx, c.Client, inp) } func (c *Client) CreateCloudFtd(ctx context.Context, inp cloudftd.CreateInput) (*cloudftd.CreateOutput, error) { - return cloudftd.Create(ctx, c.client, inp) + return cloudftd.Create(ctx, c.Client, inp) } func (c *Client) UpdateCloudFtd(ctx context.Context, inp cloudftd.UpdateInput) (*cloudftd.UpdateOutput, error) { - return cloudftd.Update(ctx, c.client, inp) + return cloudftd.Update(ctx, c.Client, inp) } func (c *Client) DeleteCloudFtd(ctx context.Context, inp cloudftd.DeleteInput) (*cloudftd.DeleteOutput, error) { - return cloudftd.Delete(ctx, c.client, inp) + return cloudftd.Delete(ctx, c.Client, inp) } func (c *Client) ReadUserByUsername(ctx context.Context, inp user.ReadByUsernameInput) (*user.ReadUserOutput, error) { - return user.ReadByUsername(ctx, c.client, inp) + return user.ReadByUsername(ctx, c.Client, inp) } func (c *Client) ReadUserByUid(ctx context.Context, inp user.ReadByUidInput) (*user.ReadUserOutput, error) { - return user.ReadByUid(ctx, c.client, inp) + return user.ReadByUid(ctx, c.Client, inp) } func (c *Client) CreateUser(ctx context.Context, inp user.CreateUserInput) (*user.CreateUserOutput, error) { - return user.Create(ctx, c.client, inp) + return user.Create(ctx, c.Client, inp) } func (c *Client) DeleteUser(ctx context.Context, inp user.DeleteUserInput) (*user.DeleteUserOutput, error) { - return user.Delete(ctx, c.client, inp) + return user.Delete(ctx, c.Client, inp) } func (c *Client) UpdateUser(ctx context.Context, inp user.UpdateUserInput) (*user.UpdateUserOutput, error) { - return user.Update(ctx, c.client, inp) + return user.Update(ctx, c.Client, inp) } func (c *Client) GenerateApiToken(ctx context.Context, inp user.GenerateApiTokenInput) (*user.ApiTokenResponse, error) { - return user.GenerateApiToken(ctx, c.client, inp) + return user.GenerateApiToken(ctx, c.Client, inp) } func (c *Client) RevokeApiToken(ctx context.Context, inp user.RevokeApiTokenInput) (*user.RevokeApiTokenOutput, error) { - return user.RevokeApiToken(ctx, c.client, inp) + return user.RevokeApiToken(ctx, c.Client, inp) } func (c *Client) CreateFtdOnboarding(ctx context.Context, inp cloudftdonboarding.CreateInput) (*cloudftdonboarding.CreateOutput, error) { - return cloudftdonboarding.Create(ctx, c.client, inp) + return cloudftdonboarding.Create(ctx, c.Client, inp) } func (c *Client) UpdateFtdOnboarding(ctx context.Context, inp cloudftdonboarding.UpdateInput) (*cloudftdonboarding.UpdateOutput, error) { - return cloudftdonboarding.Update(ctx, c.client, inp) + return cloudftdonboarding.Update(ctx, c.Client, inp) } func (c *Client) ReadFtdOnboarding(ctx context.Context, inp cloudftdonboarding.ReadInput) (*cloudftdonboarding.ReadOutput, error) { - return cloudftdonboarding.Read(ctx, c.client, inp) + return cloudftdonboarding.Read(ctx, c.Client, inp) } func (c *Client) DeleteFtdOnboarding(ctx context.Context, inp cloudftdonboarding.DeleteInput) (*cloudftdonboarding.DeleteOutput, error) { - return cloudftdonboarding.Delete(ctx, c.client, inp) + return cloudftdonboarding.Delete(ctx, c.Client, inp) } func (c *Client) ReadTenantDetails(ctx context.Context) (*tenant.ReadTenantDetailsOutput, error) { - return tenant.ReadTenantDetails(ctx, c.client) + return tenant.ReadTenantDetails(ctx, c.Client) } func (c *Client) CreateCloudFmcDevice(ctx context.Context, inp cloudfmc.CreateInput) (*cloudfmc.CreateOutput, error) { - return cloudfmc.Create(ctx, c.client, inp) + return cloudfmc.Create(ctx, c.Client, inp) } func (c *Client) ReadCloudFmcDevice(ctx context.Context) (*cloudfmc.ReadOutput, error) { - return cloudfmc.Read(ctx, c.client, cloudfmc.NewReadInput()) + return cloudfmc.Read(ctx, c.Client, cloudfmc.NewReadInput()) } func (c *Client) ReadCloudFmcSpecificDevice(ctx context.Context, inp cloudfmc.ReadSpecificInput) (*cloudfmc.ReadSpecificOutput, error) { - return cloudfmc.ReadSpecific(ctx, c.client, inp) + return cloudfmc.ReadSpecific(ctx, c.Client, inp) } func (c *Client) CreateConnectorOnboarding(ctx context.Context, inp connectoronboarding.CreateInput) (*connectoronboarding.CreateOutput, error) { - return connectoronboarding.Create(ctx, c.client, inp) + return connectoronboarding.Create(ctx, c.Client, inp) } func (c *Client) UpdateConnectorOnboarding(ctx context.Context, inp connectoronboarding.UpdateInput) (*connectoronboarding.UpdateOutput, error) { - return connectoronboarding.Update(ctx, c.client, inp) + return connectoronboarding.Update(ctx, c.Client, inp) } func (c *Client) ReadConnectorOnboarding(ctx context.Context, inp connectoronboarding.ReadInput) (*connectoronboarding.ReadOutput, error) { - return connectoronboarding.Read(ctx, c.client, inp) + return connectoronboarding.Read(ctx, c.Client, inp) } func (c *Client) DeleteConnectorOnboarding(ctx context.Context, inp connectoronboarding.DeleteInput) (*connectoronboarding.DeleteOutput, error) { - return connectoronboarding.Delete(ctx, c.client, inp) + return connectoronboarding.Delete(ctx, c.Client, inp) } func (c *Client) CreateSec(ctx context.Context, inp sec.CreateInput) (*sec.CreateOutput, error) { - return sec.Create(ctx, c.client, inp) + return sec.Create(ctx, c.Client, inp) } func (c *Client) UpdateSec(ctx context.Context, inp sec.UpdateInput) (*sec.UpdateOutput, error) { - return sec.Update(ctx, c.client, inp) + return sec.Update(ctx, c.Client, inp) } func (c *Client) DeleteSec(ctx context.Context, inp sec.DeleteInput) (*sec.DeleteOutput, error) { - return sec.Delete(ctx, c.client, inp) + return sec.Delete(ctx, c.Client, inp) } func (c *Client) ReadSec(ctx context.Context, inp sec.ReadInput) (*sec.ReadOutput, error) { - return sec.Read(ctx, c.client, inp) + return sec.Read(ctx, c.Client, inp) } func (c *Client) CreateSecOnboarding(ctx context.Context, inp seconboarding.CreateInput) (*seconboarding.CreateOutput, error) { - return seconboarding.Create(ctx, c.client, inp) + return seconboarding.Create(ctx, c.Client, inp) } func (c *Client) CreateDuoAdminPanel(ctx context.Context, inp duoadminpanel.CreateInput) (*duoadminpanel.CreateOutput, error) { - return duoadminpanel.Create(ctx, c.client, inp) + return duoadminpanel.Create(ctx, c.Client, inp) } func (c *Client) UpdateDuoAdminPanel(ctx context.Context, inp duoadminpanel.UpdateInput) (*duoadminpanel.UpdateOutput, error) { - return duoadminpanel.Update(ctx, c.client, inp) + return duoadminpanel.Update(ctx, c.Client, inp) } func (c *Client) ReadDuoAdminPanel(ctx context.Context, inp duoadminpanel.ReadByUidInput) (*duoadminpanel.ReadOutput, error) { - return duoadminpanel.ReadByUid(ctx, c.client, inp) + return duoadminpanel.ReadByUid(ctx, c.Client, inp) } func (c *Client) DeleteDuoAdminPanel(ctx context.Context, inp duoadminpanel.DeleteInput) (*duoadminpanel.DeleteOutput, error) { - return duoadminpanel.Delete(ctx, c.client, inp) + return duoadminpanel.Delete(ctx, c.Client, inp) } func (c *Client) ReadTenantSettings(ctx context.Context) (*settings.TenantSettings, error) { - return tenantsettings.Read(ctx, c.client) + return tenantsettings.Read(ctx, c.Client) } func (c *Client) UpdateTenantSettings(ctx context.Context, updateTenantSettingsInput tenantsettings.UpdateTenantSettingsInput) (*settings.TenantSettings, error) { - return tenantsettings.Update(ctx, c.client, updateTenantSettingsInput) + return tenantsettings.Update(ctx, c.Client, updateTenantSettingsInput) } func (c *Client) CreateTenantUsingMspPortal(ctx context.Context, createInput tenants.MspCreateTenantInput) (*tenants.MspTenantOutput, *tenants.CreateError) { - return tenants.Create(ctx, c.client, createInput) + return tenants.Create(ctx, c.Client, createInput) } func (c *Client) AddExistingTenantToMspPortalUsingApiToken(ctx context.Context, createInput tenants.MspAddExistingTenantInput) (*tenants.MspTenantOutput, *tenants.CreateError) { - return tenants.AddExistingTenantUsingApiToken(ctx, c.client, createInput) + return tenants.AddExistingTenantUsingApiToken(ctx, c.Client, createInput) } func (c *Client) ReadMspManagedTenantByUid(ctx context.Context, readByUidInput tenants.ReadByUidInput) (*tenants.MspTenantOutput, error) { - return tenants.ReadByUid(ctx, c.client, readByUidInput) + return tenants.ReadByUid(ctx, c.Client, readByUidInput) } func (c *Client) DeleteMspManagedTenantByUid(ctx context.Context, deleteByUidInput tenants.DeleteByUidInput) (interface{}, error) { - return tenants.DeleteByUid(ctx, c.client, deleteByUidInput) + return tenants.DeleteByUid(ctx, c.Client, deleteByUidInput) } func (c *Client) FindMspManagedTenantByName(ctx context.Context, readByNameInput tenants.ReadByNameInput) (*tenants.MspTenantsOutput, error) { - return tenants.ReadByName(ctx, c.client, readByNameInput) + return tenants.ReadByName(ctx, c.Client, readByNameInput) } func (c *Client) CreateUsersInMspManagedTenant(ctx context.Context, createInput users.MspUsersInput) (*[]users.UserDetails, *users.CreateError) { - return users.Create(ctx, c.client, createInput) + return users.Create(ctx, c.Client, createInput) } func (c *Client) ReadUsersInMspManagedTenant(ctx context.Context, readInput users.MspUsersInput) (*[]users.UserDetails, error) { - return users.ReadCreatedUsersInTenant(ctx, c.client, readInput) + return users.ReadCreatedUsersInTenant(ctx, c.Client, readInput) } func (c *Client) DeleteUsersInMspManagedTenant(ctx context.Context, deleteInput users.MspDeleteUsersInput) (interface{}, error) { - return users.Delete(ctx, c.client, deleteInput) + return users.Delete(ctx, c.Client, deleteInput) } func (c *Client) GenerateApiTokenForUserInMspManagedTenant(ctx context.Context, generateApiTokenInput users.MspGenerateApiTokenInput) (*users.MspGenerateApiTokenOutput, error) { - return users.GenerateApiToken(ctx, c.client, generateApiTokenInput) + return users.GenerateApiToken(ctx, c.Client, generateApiTokenInput) } func (c *Client) RevokeApiTokenForUserInMspManagedTenant(ctx context.Context, revokeApiTokenInput users.MspRevokeApiTokenInput) (interface{}, error) { - return users.RevokeApiToken(ctx, c.client, revokeApiTokenInput) + return users.RevokeApiToken(ctx, c.Client, revokeApiTokenInput) } func (c *Client) CreateUserGroupsInMspManagedTenant(ctx context.Context, tenantUid string, userGroups *[]usergroups.MspManagedUserGroupInput) (*[]usergroups.MspManagedUserGroup, *usergroups.CreateError) { - return usergroups.Create(ctx, c.client, tenantUid, userGroups) + return usergroups.Create(ctx, c.Client, tenantUid, userGroups) } func (c *Client) ReadUserGroupsInMspManagedTenant(ctx context.Context, tenantUid string, userGroups *[]usergroups.MspManagedUserGroupInput) (*[]usergroups.MspManagedUserGroup, error) { - return usergroups.ReadCreatedUserGroupsInTenant(ctx, c.client, tenantUid, userGroups) + return usergroups.ReadCreatedUserGroupsInTenant(ctx, c.Client, tenantUid, userGroups) } func (c *Client) DeleteUserGroupsInMspManagedTenant(ctx context.Context, tenantUid string, deleteInput *usergroups.MspManagedUserGroupDeleteInput) (interface{}, error) { - return usergroups.Delete(ctx, c.client, tenantUid, deleteInput) + return usergroups.Delete(ctx, c.Client, tenantUid, deleteInput) } diff --git a/client/device/cloudftd/read_by_uid.go b/client/device/cloudftd/read_by_uid.go index 2a57b075..52e84033 100644 --- a/client/device/cloudftd/read_by_uid.go +++ b/client/device/cloudftd/read_by_uid.go @@ -19,13 +19,18 @@ func NewReadByUidInput(uid string) ReadByUidInput { } type ReadOutput struct { - Uid string `json:"uid"` - Name string `json:"name"` - Metadata Metadata `json:"metadata,omitempty"` - State string `json:"state"` - Tags tags.Type `json:"tags"` + Uid string `json:"uid"` + DeviceType string `json:"deviceType"` + Name string `json:"name"` + Metadata Metadata `json:"metadata,omitempty"` + State string `json:"state"` + ConnectivityState int `json:"connectivityState"` + Tags tags.Type `json:"tags"` + SoftwareVersion string `json:"softwareVersion"` } +type FtdDevice = ReadOutput + func ReadByUid(ctx context.Context, client http.Client, readInp ReadByUidInput) (*ReadOutput, error) { readUrl := url.ReadDevice(client.BaseUrl(), readInp.Uid) diff --git a/client/device/cloudftd/read_upgrade_packages.go b/client/device/cloudftd/read_upgrade_packages.go new file mode 100644 index 00000000..2d1739af --- /dev/null +++ b/client/device/cloudftd/read_upgrade_packages.go @@ -0,0 +1,25 @@ +package cloudftd + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model" +) + +type UpgradePackage struct { + UpgradePackageUid string `json:"upgradePackageUid"` + SoftwareVersion string `json:"softwareVersion"` +} + +func ReadUpgradePackages(ctx context.Context, client http.Client, deviceUid string) (*[]UpgradePackage, error) { + readUrl := url.GetFtdUpgradePackagesUrl(client.BaseUrl(), deviceUid) + req := client.NewGet(ctx, readUrl) + + upgradePackageResponse := model.CdoListResponse[UpgradePackage]{} + if err := req.Send(&upgradePackageResponse); err != nil { + return nil, err + } + + return &upgradePackageResponse.Items, nil +} diff --git a/client/device/cloudftd/upgrade.go b/client/device/cloudftd/upgrade.go new file mode 100644 index 00000000..7a38e561 --- /dev/null +++ b/client/device/cloudftd/upgrade.go @@ -0,0 +1,118 @@ +package cloudftd + +import ( + "context" + "errors" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type FtdUpgradeInput struct { + Uid string `json:"uid"` + SoftwareVersion string `json:"softwareVersion"` +} + +type FtdUpgradeService interface { + Upgrade(uid string, softwareVersion string) (*FtdDevice, error) +} + +type ftdUpgradeService struct { + Ctx context.Context + Client *http.Client +} + +func NewFtdUpgradeService(ctx context.Context, client *http.Client) FtdUpgradeService { + return &ftdUpgradeService{ + Ctx: ctx, + Client: client, + } +} + +func (f *ftdUpgradeService) Upgrade(uid string, softwareVersion string) (*FtdDevice, error) { + ftdDevice, err := ReadByUid(f.Ctx, *f.Client, ReadByUidInput{Uid: uid}) + if err != nil { + return nil, err + } + tflog.Debug(f.Ctx, fmt.Sprintf("FTD device found: %v", ftdDevice)) + + tflog.Debug(f.Ctx, "Validating if FTD device is suitable for upgrade...") + err = f.validateDeviceType(ftdDevice) + if err != nil { + return nil, err + } + err = f.validateConnectivityState(ftdDevice) + if err != nil { + return nil, err + } + err = f.validateFtdVersion(ftdDevice, softwareVersion) + if err != nil { + return nil, err + } + + return ftdDevice, nil +} + +func (f *ftdUpgradeService) validateDeviceType(ftdDevice *FtdDevice) error { + if ftdDevice.DeviceType != "FTDC" { + return errors.New("this resource only supports cdFMC managed FTDs") + } + + return nil +} + +func (f *ftdUpgradeService) validateConnectivityState(ftdDevice *FtdDevice) error { + if ftdDevice.ConnectivityState != 1 { + return errors.New("FTD device connectivity state is not ONLINE. Only ONLINE devices can be upgraded") + } + + return nil +} + +func (f *ftdUpgradeService) validateFtdVersion(ftdDevice *FtdDevice, softwareVersionToUpgradeToStr string) error { + versionOnDevice, err := ftd.NewVersion(ftdDevice.SoftwareVersion) + if err != nil { + f.Client.Logger.Printf("error parsing software version %s on device\n", ftdDevice.SoftwareVersion) + return err + } + versionToUpgradeTo, err := ftd.NewVersion(softwareVersionToUpgradeToStr) + if err != nil { + f.Client.Logger.Printf("error parsing software version %s to upgrade to\n", softwareVersionToUpgradeToStr) + return err + } + + if versionOnDevice.GreaterThan(versionToUpgradeTo) { + return errors.New(fmt.Sprintf("FTD device is on version %s, which is newer than the"+ + " version to upgrade to: %s", ftdDevice.SoftwareVersion, softwareVersionToUpgradeToStr)) + } + if versionOnDevice.LessThan(versionToUpgradeTo) { + err = f.validateUpgradePathExistsTo(ftdDevice, versionToUpgradeTo) + if err != nil { + return err + } + return errors.New("upgrade implementation coming soon") + } + + return nil +} + +func (f *ftdUpgradeService) validateUpgradePathExistsTo(ftdDevice *FtdDevice, toVersion *ftd.Version) error { + upgradePackages, err := ReadUpgradePackages(f.Ctx, *f.Client, ftdDevice.Uid) + if err != nil { + return err + } + for _, upgradePackage := range *upgradePackages { + tflog.Debug(f.Ctx, fmt.Sprintf("Checking upgrade package: %s", upgradePackage.SoftwareVersion)) + softwareVersion, err := ftd.NewVersion(upgradePackage.SoftwareVersion) + if err != nil { + f.Client.Logger.Printf("error parsing software version %s in upgrade package\n", upgradePackage.SoftwareVersion) + return err + } + if softwareVersion.Equal(toVersion) { + return nil + } + } + + return errors.New(fmt.Sprintf("%s is not a valid version to upgrade FTD device %s to", toVersion.String(), ftdDevice.Name)) +} diff --git a/client/device/cloudftd/upgrade_test.go b/client/device/cloudftd/upgrade_test.go new file mode 100644 index 00000000..58bb4209 --- /dev/null +++ b/client/device/cloudftd/upgrade_test.go @@ -0,0 +1,249 @@ +package cloudftd_test + +import ( + "errors" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model" + "github.com/google/uuid" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + mockhttp "net/http" + "testing" + "time" +) + +var upgradePackages []cloudftd.UpgradePackage = []cloudftd.UpgradePackage{ + { + UpgradePackageUid: uuid.New().String(), + SoftwareVersion: "7.2.5.1-29", + }, + { + UpgradePackageUid: uuid.New().String(), + SoftwareVersion: "7.2.6-293", + }, +} + +func TestUpgrade(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + testCases := []struct { + testName string + uid string + softwareVersion string + expectedFtdDevice *cloudftd.FtdDevice + expectedError error + setupFunc func(deviceUid string, softwareVersion string, ftdDevice *cloudftd.FtdDevice) + assertFunc func(ftdDevice *cloudftd.FtdDevice, err error, expectedFtdDevice *cloudftd.FtdDevice, expectedError error, t *testing.T) + }{ + { + testName: "Fail to upgrade if FTD device not found", + uid: uuid.New().String(), + softwareVersion: "7.2.5", + expectedFtdDevice: nil, + expectedError: nil, + setupFunc: func(deviceUid string, softwareVersion string, ftdDevice *cloudftd.FtdDevice) { + httpmock.RegisterResponder( + mockhttp.MethodGet, + baseUrl+"/aegis/rest/v1/services/targets/devices/"+deviceUid, + httpmock.NewStringResponder(404, ""), + ) + }, + assertFunc: func(ftdDevice *cloudftd.FtdDevice, err error, expectedFtdDevice *cloudftd.FtdDevice, expectedError error, t *testing.T) { + assert.Nil(t, ftdDevice) + assert.NotNil(t, err) + }, + }, + { + testName: "Fail to upgrade if FTD device connectivity state is not ONLINE", + uid: uuid.New().String(), + softwareVersion: "7.2.5", + expectedFtdDevice: nil, + expectedError: errors.New("FTD device connectivity state is not ONLINE. Only ONLINE devices can be upgraded"), + setupFunc: func(deviceUid string, softwareVersion string, ftdDevice *cloudftd.FtdDevice) { + httpmock.RegisterResponder( + mockhttp.MethodGet, + baseUrl+"/aegis/rest/v1/services/targets/devices/"+deviceUid, + httpmock.NewJsonResponderOrPanic(200, &cloudftd.FtdDevice{ + Uid: deviceUid, + Name: "FTD Device", + Metadata: cloudftd.Metadata{}, + State: "ACTIVE", + DeviceType: "FTDC", + ConnectivityState: -3, + Tags: nil, + SoftwareVersion: "7.2.4", + }), + ) + }, + assertFunc: func(ftdDevice *cloudftd.FtdDevice, err error, expectedFtdDevice *cloudftd.FtdDevice, expectedError error, t *testing.T) { + assert.Nil(t, ftdDevice) + assert.NotNil(t, err) + assert.Equal(t, expectedError.Error(), err.Error()) + }, + }, + { + testName: "Fail to upgrade if device is not cdFMC-managed FTD", + uid: uuid.New().String(), + softwareVersion: "7.2.5", + expectedFtdDevice: nil, + expectedError: errors.New("this resource only supports cdFMC managed FTDs"), + setupFunc: func(deviceUid string, softwareVersion string, ftdDevice *cloudftd.FtdDevice) { + httpmock.RegisterResponder( + mockhttp.MethodGet, + baseUrl+"/aegis/rest/v1/services/targets/devices/"+deviceUid, + httpmock.NewJsonResponderOrPanic(200, &cloudftd.FtdDevice{ + Uid: deviceUid, + Name: "FTD Device", + Metadata: cloudftd.Metadata{}, + State: "ACTIVE", + DeviceType: "ASA", + ConnectivityState: 1, + Tags: nil, + SoftwareVersion: "7.2.4", + }), + ) + }, + assertFunc: func(ftdDevice *cloudftd.FtdDevice, err error, expectedFtdDevice *cloudftd.FtdDevice, expectedError error, t *testing.T) { + assert.Nil(t, ftdDevice) + assert.NotNil(t, err) + assert.Equal(t, expectedError.Error(), err.Error()) + }, + }, + { + testName: "Fail to upgrade if FTD device software version is less than version to upgrade to", + uid: uuid.New().String(), + softwareVersion: "7.2.5", + expectedFtdDevice: nil, + expectedError: errors.New("FTD device is on version 7.3.0, which is newer than the version to upgrade to: 7.2.5"), + setupFunc: func(deviceUid string, softwareVersion string, ftdDevice *cloudftd.FtdDevice) { + httpmock.RegisterResponder( + mockhttp.MethodGet, + baseUrl+"/aegis/rest/v1/services/targets/devices/"+deviceUid, + httpmock.NewJsonResponderOrPanic(200, &cloudftd.FtdDevice{ + Uid: deviceUid, + Name: "FTD Device", + Metadata: cloudftd.Metadata{}, + State: "ACTIVE", + DeviceType: "FTDC", + ConnectivityState: 1, + Tags: nil, + SoftwareVersion: "7.3.0", + }), + ) + }, + assertFunc: func(ftdDevice *cloudftd.FtdDevice, err error, expectedFtdDevice *cloudftd.FtdDevice, expectedError error, t *testing.T) { + assert.Nil(t, ftdDevice) + assert.NotNil(t, err) + assert.Equal(t, expectedError.Error(), err.Error()) + }, + }, + { + testName: "Do not fail if device software version is equal to version to upgrade to", + uid: uuid.New().String(), + softwareVersion: "7.2.5", + expectedFtdDevice: &cloudftd.FtdDevice{ + Uid: uuid.New().String(), + Name: "FTD Device", + Metadata: cloudftd.Metadata{}, + State: "ACTIVE", + ConnectivityState: 1, + DeviceType: "FTDC", + Tags: nil, + SoftwareVersion: "7.2.5", + }, + expectedError: nil, + setupFunc: func(deviceUid string, softwareVersion string, ftdDevice *cloudftd.FtdDevice) { + httpmock.RegisterResponder(mockhttp.MethodGet, + baseUrl+"/aegis/rest/v1/services/targets/devices/"+deviceUid, + httpmock.NewJsonResponderOrPanic(200, ftdDevice)) + }, + assertFunc: func(ftdDevice *cloudftd.FtdDevice, err error, expectedFtdDevice *cloudftd.FtdDevice, expectedError error, t *testing.T) { + assert.NotNil(t, ftdDevice) + assert.Nil(t, err) + assert.Equal(t, expectedFtdDevice, ftdDevice) + }, + }, + { + testName: "Upgrade FTD device - fail because the specified version is incompatible", + uid: uuid.New().String(), + softwareVersion: "7.2.5", + expectedFtdDevice: &cloudftd.FtdDevice{ + Uid: uuid.New().String(), + Name: "FTD Device", + Metadata: cloudftd.Metadata{}, + State: "ACTIVE", + DeviceType: "FTDC", + ConnectivityState: 1, + Tags: nil, + SoftwareVersion: "7.2.3", + }, + expectedError: errors.New("7.2.5 is not a valid version to upgrade FTD device FTD Device to"), + setupFunc: func(deviceUid string, softwareVersion string, ftdDevice *cloudftd.FtdDevice) { + httpmock.RegisterResponder(mockhttp.MethodGet, + baseUrl+"/aegis/rest/v1/services/targets/devices/"+deviceUid, + httpmock.NewJsonResponderOrPanic(200, ftdDevice)) + httpmock.RegisterResponder(mockhttp.MethodGet, baseUrl+"/api/rest/v1/inventory/devices/ftds/"+ftdDevice.Uid+"/upgrades/versions", httpmock.NewJsonResponderOrPanic(200, model.CdoListResponse[cloudftd.UpgradePackage]{ + Items: upgradePackages, + Count: len(upgradePackages), + })) + }, + assertFunc: func(ftdDevice *cloudftd.FtdDevice, err error, expectedFtdDevice *cloudftd.FtdDevice, expectedError error, t *testing.T) { + assert.Nil(t, ftdDevice) + assert.NotNil(t, err) + assert.Equal(t, expectedError.Error(), err.Error()) + }, + }, + // TODO this test should be changed to a success test once the upgrade implementation is done + { + testName: "Upgrade FTD device - fail because the code has not been implemented yet", + uid: uuid.New().String(), + softwareVersion: "7.2.5.1-29", + expectedFtdDevice: &cloudftd.FtdDevice{ + Uid: uuid.New().String(), + Name: "FTD Device", + Metadata: cloudftd.Metadata{}, + State: "ACTIVE", + DeviceType: "FTDC", + ConnectivityState: 1, + Tags: nil, + SoftwareVersion: "7.2.3", + }, + expectedError: errors.New("upgrade implementation coming soon"), + setupFunc: func(deviceUid string, softwareVersion string, ftdDevice *cloudftd.FtdDevice) { + httpmock.RegisterResponder(mockhttp.MethodGet, + baseUrl+"/aegis/rest/v1/services/targets/devices/"+deviceUid, + httpmock.NewJsonResponderOrPanic(200, ftdDevice)) + httpmock.RegisterResponder(mockhttp.MethodGet, baseUrl+"/api/rest/v1/inventory/devices/ftds/"+ftdDevice.Uid+"/upgrades/versions", httpmock.NewJsonResponderOrPanic(200, model.CdoListResponse[cloudftd.UpgradePackage]{ + Items: upgradePackages, + Count: len(upgradePackages), + })) + }, + assertFunc: func(ftdDevice *cloudftd.FtdDevice, err error, expectedFtdDevice *cloudftd.FtdDevice, expectedError error, t *testing.T) { + assert.Nil(t, ftdDevice) + assert.NotNil(t, err) + assert.Equal(t, expectedError.Error(), err.Error()) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc(testCase.uid, testCase.softwareVersion, testCase.expectedFtdDevice) + + ftdDevice, err := cloudftd.NewFtdUpgradeService( + context.Background(), + http.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), + ).Upgrade( + testCase.uid, + testCase.softwareVersion, + ) + + testCase.assertFunc(ftdDevice, err, testCase.expectedFtdDevice, testCase.expectedError, t) + }) + } +} diff --git a/client/go.mod b/client/go.mod index 9c291e09..f39e1535 100644 --- a/client/go.mod +++ b/client/go.mod @@ -1,6 +1,8 @@ module github.com/CiscoDevnet/terraform-provider-cdo/go-client -go 1.20 +go 1.21 + +toolchain go1.23.1 require ( github.com/google/uuid v1.6.0 diff --git a/client/go.sum b/client/go.sum index 109f7bc4..792ea1b9 100644 --- a/client/go.sum +++ b/client/go.sum @@ -1,3 +1,6 @@ +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -6,13 +9,16 @@ github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nu github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= diff --git a/client/internal/url/url.go b/client/internal/url/url.go index c9faebc6..8c1a4ae7 100644 --- a/client/internal/url/url.go +++ b/client/internal/url/url.go @@ -261,3 +261,7 @@ func GetCompatibleAsaVersions(baseUrl string, deviceUid string) string { func GetUpgradeAsaUrl(baseUrl string, deviceUid string) string { return fmt.Sprintf("%s/api/rest/v1/inventory/devices/asas/%s/upgrades/trigger", baseUrl, deviceUid) } + +func GetFtdUpgradePackagesUrl(baseUrl string, deviceUid string) string { + return fmt.Sprintf("%s/api/rest/v1/inventory/devices/ftds/%s/upgrades/versions", baseUrl, deviceUid) +} diff --git a/client/model/ftd/ftd_version.go b/client/model/ftd/ftd_version.go new file mode 100644 index 00000000..1b6167f0 --- /dev/null +++ b/client/model/ftd/ftd_version.go @@ -0,0 +1,212 @@ +package ftd + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strconv" +) + +// The compiled version of the regex created at init() is cached here so it +// only needs to be created once. +var versionRegex *regexp.Regexp + +// semVerRegex is the regular expression used to parse a semantic version. +// This is not the official regex from the semver spec. It has been modified to allow for loose handling +// where versions like 2.1 are detected. +const semVerRegex = `^(\d+)\.(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-(\d+))?$` + +// Version represents a single semantic version. +type Version struct { + major, minor, patch, hotfix uint64 + buildNum uint64 + original string +} + +func init() { + versionRegex = regexp.MustCompile("^" + semVerRegex + "$") +} + +const ( + num string = "0123456789" + allowed string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + num +) + +// NewVersion parses a given version and returns an instance of Version or +// an error if unable to parse the version. If the version is SemVer-ish it +// attempts to convert it to SemVer. If you want to validate it was a strict +// semantic version at parse time see StrictNewVersion(). +func NewVersion(v string) (*Version, error) { + m := versionRegex.FindStringSubmatch(v) + if m == nil { + return nil, errors.New(fmt.Sprintf("invalid FTD Version %s", v)) + } + + sv := &Version{ + original: v, + } + + var err error + sv.major, err = strconv.ParseUint(m[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("Error parsing version segment: %s", err) + } + + if m[2] != "" { + sv.minor, err = strconv.ParseUint(m[2], 10, 64) + if err != nil { + return nil, fmt.Errorf("Error parsing version segment: %s", err) + } + } else { + sv.minor = 0 + } + + if m[3] != "" { + sv.patch, err = strconv.ParseUint(m[3], 10, 64) + if err != nil { + return nil, fmt.Errorf("Error parsing version segment: %s", err) + } + } else { + sv.patch = 0 + } + + if m[4] != "" { + sv.hotfix, err = strconv.ParseUint(m[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("Error parsing version segment: %s", err) + } + } else { + sv.hotfix = 0 + } + + if m[5] != "" { + sv.buildNum, err = strconv.ParseUint(m[5], 10, 64) + if err != nil { + return nil, fmt.Errorf("Error parsing version segment: %s", err) + } + } else { + sv.buildNum = 0 + } + + return sv, nil +} + +// String converts a Version object to a string. +// Note, if the original version contained a leading v this version will not. +// See the Original() method to retrieve the original value. Semantic Versions +// don't contain a leading v per the spec. Instead it's optional on +// implementation. +func (v Version) String() string { + var buf bytes.Buffer + + fmt.Fprintf(&buf, "%d.%d.%d", v.major, v.minor, v.patch) + if v.hotfix != 0 { + fmt.Fprintf(&buf, ".%d", v.hotfix) + } + if v.buildNum != 0 { + fmt.Fprintf(&buf, "-%d", v.buildNum) + } + + return buf.String() +} + +// Original returns the original value passed in to be parsed. +func (v *Version) Original() string { + return v.original +} + +// Major returns the major version. +func (v Version) Major() uint64 { + return v.major +} + +// Minor returns the minor version. +func (v Version) Minor() uint64 { + return v.minor +} + +// Patch returns the patch version. +func (v Version) Patch() uint64 { + return v.patch +} + +func (v Version) Hotfix() uint64 { + return v.hotfix +} + +// Prerelease returns the pre-release version. +func (v Version) Buildnum() uint64 { + return v.buildNum +} + +// LessThan tests if one version is less than another one. +func (v *Version) LessThan(o *Version) bool { + return v.Compare(o) < 0 +} + +// LessThanEqual tests if one version is less or equal than another one. +func (v *Version) LessThanEqual(o *Version) bool { + return v.Compare(o) <= 0 +} + +// GreaterThan tests if one version is greater than another one. +func (v *Version) GreaterThan(o *Version) bool { + return v.Compare(o) > 0 +} + +// GreaterThanEqual tests if one version is greater or equal than another one. +func (v *Version) GreaterThanEqual(o *Version) bool { + return v.Compare(o) >= 0 +} + +// Equal tests if two versions are equal to each other. +// Note, versions can be equal with different metadata since metadata +// is not considered part of the comparable version. +func (v *Version) Equal(o *Version) bool { + if v == o { + return true + } + if v == nil || o == nil { + return false + } + return v.Compare(o) == 0 +} + +// Compare compares this version to another one. It returns -1, 0, or 1 if +// the version smaller, equal, or larger than the other version. +// +// Versions are compared by X.Y.Z. Build metadata is ignored. Prerelease is +// lower than the version without a prerelease. Compare always takes into account +// prereleases. If you want to work with ranges using typical range syntaxes that +// skip prereleases if the range is not looking for them use constraints. +func (v *Version) Compare(o *Version) int { + if d := compareSegment(v.Major(), o.Major()); d != 0 { + return d + } + if d := compareSegment(v.Minor(), o.Minor()); d != 0 { + return d + } + if d := compareSegment(v.Patch(), o.Patch()); d != 0 { + return d + } + if d := compareSegment(v.Hotfix(), o.Hotfix()); d != 0 { + return d + } + if d := compareSegment(v.Buildnum(), o.Buildnum()); d != 0 { + return d + } + + return 0 +} + +func compareSegment(v, o uint64) int { + if v < o { + return -1 + } + if v > o { + return 1 + } + + return 0 +} diff --git a/client/model/ftd/ftd_version_test.go b/client/model/ftd/ftd_version_test.go new file mode 100644 index 00000000..4ab99c38 --- /dev/null +++ b/client/model/ftd/ftd_version_test.go @@ -0,0 +1,152 @@ +package ftd_test + +import ( + "errors" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewFtdVersion(t *testing.T) { + testCases := []struct { + testName string + versionStr string + expectedErr error + }{ + { + testName: "Successfully parse major.minor.patch", + versionStr: "7.2.0", + expectedErr: nil, + }, + { + testName: "Successfully parse major.minor.patch-buildnum", + versionStr: "7.2.0-69", + expectedErr: nil, + }, + { + testName: "Successfully parse major.minor.patch.hotfix", + versionStr: "7.2.1.45", + expectedErr: nil, + }, + { + testName: "Successfully parse major.minor.patch.hotfix-buildnumber", + versionStr: "7.2.1.45-59", + expectedErr: nil, + }, + { + testName: "Fail to parse invalid FTD version", + versionStr: "9.8.4(100)1", + expectedErr: errors.New("invalid FTD Version 9.8.4(100)1"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + version, err := ftd.NewVersion(testCase.versionStr) + if err == nil { + assert.Equal(t, testCase.versionStr, version.String()) + } + assert.Equal(t, testCase.expectedErr, err) + }) + } +} + +func TestFtdVersionComparison(t *testing.T) { + testCases := []struct { + testName string + versionOneStr string + versionTwoStr string + assertFunc func(t *testing.T, versionOne, versionTwo *ftd.Version) + }{ + { + testName: "Compare major.minor.patch versions", + versionOneStr: "7.2.0", + versionTwoStr: "7.3.0", + assertFunc: func(t *testing.T, versionOne, versionTwo *ftd.Version) { + assert.True(t, versionOne.LessThan(versionTwo)) + assert.True(t, versionTwo.GreaterThan(versionOne)) + assert.False(t, versionTwo.Equal(versionOne)) + }, + }, + { + testName: "Compare major.minor.patch versions semantically", + versionOneStr: "7.1.2", + versionTwoStr: "7.3.0", + assertFunc: func(t *testing.T, versionOne, versionTwo *ftd.Version) { + assert.True(t, versionOne.LessThan(versionTwo)) + assert.True(t, versionTwo.GreaterThan(versionOne)) + assert.False(t, versionTwo.Equal(versionOne)) + }, + }, + { + testName: "Compare more major.minor.patch versions semantically", + versionOneStr: "7.8.0", + versionTwoStr: "7.12.0", + assertFunc: func(t *testing.T, versionOne, versionTwo *ftd.Version) { + assert.True(t, versionOne.LessThan(versionTwo)) + assert.True(t, versionTwo.GreaterThan(versionOne)) + assert.False(t, versionTwo.Equal(versionOne)) + }, + }, + { + testName: "Compare equal major.minor.patch versions", + versionOneStr: "7.2.0", + versionTwoStr: "7.2.0", + assertFunc: func(t *testing.T, versionOne, versionTwo *ftd.Version) { + assert.False(t, versionOne.LessThan(versionTwo)) + assert.False(t, versionTwo.LessThan(versionOne)) + assert.True(t, versionTwo.Equal(versionOne)) + }, + }, + { + testName: "Compare major.minor.patch-buildnum versions", + versionOneStr: "7.2.0-68", + versionTwoStr: "7.2.0-69", + assertFunc: func(t *testing.T, versionOne, versionTwo *ftd.Version) { + assert.True(t, versionOne.LessThan(versionTwo)) + assert.True(t, versionTwo.GreaterThan(versionOne)) + assert.False(t, versionTwo.Equal(versionOne)) + }, + }, + { + testName: "Compare equal major.minor.patch-buildnum versions", + versionOneStr: "7.2.0-65", + versionTwoStr: "7.2.0-65", + assertFunc: func(t *testing.T, versionOne, versionTwo *ftd.Version) { + assert.False(t, versionOne.LessThan(versionTwo)) + assert.False(t, versionTwo.LessThan(versionOne)) + assert.True(t, versionTwo.Equal(versionOne)) + }, + }, + { + testName: "Compare major.minor.patch-buildnum versions", + versionOneStr: "7.2.0.2-68", + versionTwoStr: "7.2.0.3-69", + assertFunc: func(t *testing.T, versionOne, versionTwo *ftd.Version) { + assert.True(t, versionOne.LessThan(versionTwo)) + assert.True(t, versionTwo.GreaterThan(versionOne)) + assert.False(t, versionTwo.Equal(versionOne)) + }, + }, + { + testName: "Compare equal major.minor.patch.hotfix-buildnum versions", + versionOneStr: "7.2.1.24-65", + versionTwoStr: "7.2.1.24-65", + assertFunc: func(t *testing.T, versionOne, versionTwo *ftd.Version) { + assert.False(t, versionOne.LessThan(versionTwo)) + assert.False(t, versionTwo.LessThan(versionOne)) + assert.True(t, versionTwo.Equal(versionOne)) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + parsedVersionOne, err := ftd.NewVersion(testCase.versionOneStr) + assert.Nil(t, err) + parsedVersionTwo, err := ftd.NewVersion(testCase.versionTwoStr) + assert.Nil(t, err) + testCase.assertFunc(t, parsedVersionOne, parsedVersionTwo) + }) + } +} diff --git a/docs/resources/ftd_device_version.md b/docs/resources/ftd_device_version.md new file mode 100644 index 00000000..6823c1b7 --- /dev/null +++ b/docs/resources/ftd_device_version.md @@ -0,0 +1,26 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdo_ftd_device_version Resource - cdo" +subcategory: "" +description: |- + Provides a resource to upgrade the software version of an FTD device. Note: The FTD device has to already have been added to the Terraform state using a resource or a data source. +--- + +# cdo_ftd_device_version (Resource) + +Provides a resource to upgrade the software version of an FTD device. Note: The FTD device has to already have been added to the Terraform state using a resource or a data source. + + + +<!-- schema generated by tfplugindocs --> +## Schema + +### Required + +- `ftd_uid` (String) The unique identifier of the FTD device to upgrade. +- `software_version` (String) The software version to upgrade the FTD device to. + +### Read-Only + +- `id` (String) The unique identifier of the FTD version resource +- `software_version_on_device` (String) The software version currently on the FTD device. diff --git a/go.work b/go.work index 1d324d8e..aa595dce 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,6 @@ -go 1.20 +go 1.21 + +toolchain go1.23.1 use ( ./client diff --git a/provider/examples/resources/ftd-version/README.md b/provider/examples/resources/ftd-version/README.md new file mode 100644 index 00000000..8e354dfe --- /dev/null +++ b/provider/examples/resources/ftd-version/README.md @@ -0,0 +1,7 @@ +# FTD Example + +## Usage +1. Modify `terraform.tfvars` +2. Paste CDO API token into `api_token.txt` + - see https://docs.defenseorchestrator.com/#!c-api-tokens.html for how to generate this. +3. Change the `base_url` in `provider.tf` to the appropriate URL \ No newline at end of file diff --git a/provider/examples/resources/ftd-version/api_token.txt b/provider/examples/resources/ftd-version/api_token.txt new file mode 100644 index 00000000..e69de29b diff --git a/provider/examples/resources/ftd-version/main.tf b/provider/examples/resources/ftd-version/main.tf new file mode 100644 index 00000000..1987c510 --- /dev/null +++ b/provider/examples/resources/ftd-version/main.tf @@ -0,0 +1,8 @@ +data "cdo_ftd_device" "ftd" { + name = var.ftd_name +} + +resource "cdo_ftd_device_version" "ftd" { + ftd_uid = data.cdo_ftd_device.ftd.id + software_version = "7.2.5-208" +} \ No newline at end of file diff --git a/provider/examples/resources/ftd-version/providers.tf b/provider/examples/resources/ftd-version/providers.tf new file mode 100644 index 00000000..e13cff70 --- /dev/null +++ b/provider/examples/resources/ftd-version/providers.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + cdo = { + source = "CiscoDevnet/cdo" + } + } +} + +provider "cdo" { + base_url = "<https://www.defenseorchestrator.com|https://www.defenseorchestrator.eu|https://apj.cdo.cisco.com|https://aus.cdo.cisco.com|https://in.cdo.cisco.com>" + api_token = file("${path.module}/api_token.txt") +} \ No newline at end of file diff --git a/provider/examples/resources/ftd-version/terraform.tfvars b/provider/examples/resources/ftd-version/terraform.tfvars new file mode 100644 index 00000000..aadb1557 --- /dev/null +++ b/provider/examples/resources/ftd-version/terraform.tfvars @@ -0,0 +1 @@ +ftd_name = "ftd-to-upgrade-1" \ No newline at end of file diff --git a/provider/examples/resources/ftd-version/variables.tf b/provider/examples/resources/ftd-version/variables.tf new file mode 100644 index 00000000..720d70bb --- /dev/null +++ b/provider/examples/resources/ftd-version/variables.tf @@ -0,0 +1,3 @@ +variable "ftd_name" { + description = "Name of the FTDv in CDO to upgrade" +} \ No newline at end of file diff --git a/provider/internal/device/ftd/ftdversion/resource.go b/provider/internal/device/ftd/ftdversion/resource.go new file mode 100644 index 00000000..53676675 --- /dev/null +++ b/provider/internal/device/ftd/ftdversion/resource.go @@ -0,0 +1,157 @@ +package ftdversion + +import ( + "context" + "fmt" + cdoClient "github.com/CiscoDevnet/terraform-provider-cdo/go-client" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var _ resource.Resource = &Resource{} + +func NewResource() resource.Resource { + return &Resource{} +} + +type Resource struct { + client *cdoClient.Client +} + +type ResourceModel struct { + Id types.String `tfsdk:"id"` + FtdUid types.String `tfsdk:"ftd_uid"` + SoftwareVersion types.String `tfsdk:"software_version"` + SoftwareVersionOnDevice types.String `tfsdk:"software_version_on_device"` +} + +func (r *Resource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_ftd_device_version" +} + +func (r *Resource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + MarkdownDescription: "Provides a resource to upgrade the software version of an FTD device." + + " Note: The FTD device has to already have been added to the Terraform state using a " + + "resource or a data source.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The unique identifier of the FTD version resource", + Computed: true, + }, + "ftd_uid": schema.StringAttribute{ + MarkdownDescription: "The unique identifier of the FTD device to upgrade.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "software_version": schema.StringAttribute{ + MarkdownDescription: "The software version to upgrade the FTD device to.", + Required: true, + }, + "software_version_on_device": schema.StringAttribute{ + MarkdownDescription: "The software version currently on the FTD device.", + Computed: true, + }, + }, + } +} + +func (r *Resource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if request.ProviderData == nil { + return + } + + client, ok := request.ProviderData.(*cdoClient.Client) + + if !ok { + response.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *cdoClient.Client, got: %T. Please report this issue to the provider developers.", request.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + tflog.Debug(ctx, "Create a new FTD version resource...") + + var planData ResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &planData)...) + if response.Diagnostics.HasError() { + return + } + + ftdDevice, err := r.upgrade(ctx, planData.FtdUid.ValueString(), planData.SoftwareVersion.ValueString()) + if err != nil { + response.Diagnostics.AddError("Failed to upgrade FTD device...", err.Error()) + return + } + + tflog.Debug(ctx, fmt.Sprintf("FTD device upgraded successfully: %v", ftdDevice)) + planData.Id = planData.FtdUid + planData.SoftwareVersionOnDevice = types.StringValue(ftdDevice.SoftwareVersion) + response.Diagnostics.Append(response.State.Set(ctx, &planData)...) +} + +func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + tflog.Debug(ctx, "Reading FTD device to update the upgrade resource...") + var stateData ResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &stateData)...) + if response.Diagnostics.HasError() { + return + } + + ftdDevice, err := cloudftd.ReadByUid(ctx, r.client.Client, cloudftd.ReadByUidInput{Uid: stateData.FtdUid.ValueString()}) + if err != nil { + response.Diagnostics.AddError("Failed to read FTD device...", err.Error()) + return + } + tflog.Debug(ctx, fmt.Sprintf("FTD device found: %v", ftdDevice)) + + stateData.SoftwareVersionOnDevice = types.StringValue(ftdDevice.SoftwareVersion) + response.Diagnostics.Append(response.State.Set(ctx, &stateData)...) +} + +func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + tflog.Debug(ctx, "Update a new FTD version resource...") + + var planData ResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &planData)...) + if response.Diagnostics.HasError() { + return + } + + ftdDevice, err := r.upgrade(ctx, planData.FtdUid.ValueString(), planData.SoftwareVersion.ValueString()) + if err != nil { + response.Diagnostics.AddError("Failed to upgrade FTD device...", err.Error()) + return + } + + tflog.Debug(ctx, fmt.Sprintf("FTD device upgraded successfully: %v", ftdDevice)) + response.Diagnostics.Append(response.State.Set(ctx, &planData)...) +} + +func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + tflog.Info(ctx, "Removing a version resource is a noop. It will not trigger a revert of the upgrade on the FTD device.") +} + +func (r *Resource) upgrade(ctx context.Context, deviceUid string, softwareVersion string) (*cloudftd.FtdDevice, error) { + ftdUpgradeService := cloudftd.NewFtdUpgradeService(ctx, &r.client.Client) + ftdDevice, err := ftdUpgradeService.Upgrade(deviceUid, softwareVersion) + if err != nil { + return nil, err + } + + return ftdDevice, nil +} diff --git a/provider/internal/device/ftd/ftdversion/resource_test.go b/provider/internal/device/ftd/ftdversion/resource_test.go new file mode 100644 index 00000000..61eb9b11 --- /dev/null +++ b/provider/internal/device/ftd/ftdversion/resource_test.go @@ -0,0 +1,42 @@ +package ftdversion_test + +import ( + "github.com/CiscoDevnet/terraform-provider-cdo/internal/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "testing" +) + +var dataSourceModel = struct { + Name string +}{ + Name: acctest.Env.FtdDataSourceName(), // typically we get the actual value from the environment like this: acctest.Env.ExampleDataSourceName(), so that most parameters are configurable so that it can be run in different CDO environment +} + +var sameVersionTemplate = ` +data "cdo_ftd_device" "test" { + name = "{{.Name}}" +} + +resource "cdo_ftd_device_version" "test" { + ftd_uid = data.cdo_ftd_device.test.id + software_version = "7.3.0" +} +` + +var config = acctest.MustParseTemplate(sameVersionTemplate, dataSourceModel) + +func TestAccFtdVersionResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: acctest.PreCheckFunc(t), + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // when the software_version specified in cdo_ftd_device_version is the same as the version on the FTD, then I should not fail + { + Config: acctest.ProviderConfig() + config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("cdo_ftd_device_version.test", "software_version_on_device", "7.3.0"), + ), + }, + }, + }) +} diff --git a/provider/internal/provider/provider.go b/provider/internal/provider/provider.go index c90f000e..e4881600 100644 --- a/provider/internal/provider/provider.go +++ b/provider/internal/provider/provider.go @@ -6,6 +6,7 @@ package provider import ( "context" "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/internal/device/ftd/ftdversion" "github.com/CiscoDevnet/terraform-provider-cdo/internal/msp/msp_tenant" "github.com/CiscoDevnet/terraform-provider-cdo/internal/msp/msp_tenant_user_api_token" "github.com/CiscoDevnet/terraform-provider-cdo/internal/msp/msp_tenant_user_groups" @@ -180,6 +181,7 @@ func (p *CdoProvider) Resources(ctx context.Context) []func() resource.Resource msp_tenant_users.NewMspManagedTenantUsersResource, msp_tenant_user_api_token.NewMspManagedTenantUserApiTokenResource, msp_tenant_user_groups.NewMspManagedTenantUserGroupsResource, + ftdversion.NewResource, } }