Skip to content

Commit

Permalink
LH-89808: Implement upgrade to ftd version. (#159)
Browse files Browse the repository at this point in the history
LH-89808: Fix test case for equal versions.

LH-89808: Simplify code to remove unnecessary client calls.

LH-89808: Simplify code to remove unnecessary client calls.
  • Loading branch information
isak-cisco authored Feb 10, 2025
1 parent 7f48480 commit 63d1766
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 28 deletions.
75 changes: 55 additions & 20 deletions client/device/cloudftd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import (
"errors"
"fmt"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/retry"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd"
"github.com/hashicorp/terraform-plugin-log/tflog"
"time"
)

type FtdUpgradeInput struct {
Uid string `json:"uid"`
SoftwareVersion string `json:"softwareVersion"`
UpgradePackageUid string `json:"upgradePackageUid"`
}

type FtdUpgradeService interface {
Expand All @@ -30,7 +33,9 @@ func NewFtdUpgradeService(ctx context.Context, client *http.Client) FtdUpgradeSe
}
}

func (f *ftdUpgradeService) Upgrade(uid string, softwareVersion string) (*FtdDevice, error) {
func (f *ftdUpgradeService) Upgrade(uid string, softwareVersionStr string) (*FtdDevice, error) {
var upgradePackage *UpgradePackage
var newSoftwareVersion *ftd.Version
ftdDevice, err := ReadByUid(f.Ctx, *f.Client, ReadByUidInput{Uid: uid})
if err != nil {
return nil, err
Expand All @@ -46,11 +51,46 @@ func (f *ftdUpgradeService) Upgrade(uid string, softwareVersion string) (*FtdDev
if err != nil {
return nil, err
}
err = f.validateFtdVersion(ftdDevice, softwareVersion)
newSoftwareVersion, err = f.validateFtdVersion(ftdDevice, softwareVersionStr)
if err != nil {
return nil, err
}
if newSoftwareVersion == nil {
tflog.Debug(f.Ctx, "New software version is the same as the current software version. No upgrade needed.")
return ftdDevice, nil
}
upgradePackage, err = f.validateUpgradePathExistsTo(ftdDevice, newSoftwareVersion)
if err != nil {
return nil, err
}

tflog.Debug(f.Ctx, "Triggering the FTD device upgrade.")
return f.doUpgrade(upgradePackage, ftdDevice)
}

func (f *ftdUpgradeService) doUpgrade(upgradePackage *UpgradePackage, ftdDevice *FtdDevice) (*FtdDevice, error) {
upgradeUrl := url.GetFtdUpgradeUrl(f.Client.BaseUrl(), ftdDevice.Uid)
transaction, err := publicapi.TriggerTransaction(f.Ctx, *f.Client, upgradeUrl, FtdUpgradeInput{
UpgradePackageUid: upgradePackage.UpgradePackageUid,
})
if err != nil {
return nil, err
}

// poll every 30 seconds for up to 60 minutes
_, err = publicapi.WaitForTransactionToFinish(f.Ctx, *f.Client, transaction, retry.NewOptionsBuilder().
Logger(f.Client.Logger).
Timeout(60*time.Minute).
Retries(-1).
EarlyExitOnError(true).
Message(fmt.Sprintf("Upgrading FTD device to version: %s)", upgradePackage.SoftwareVersion)).
Delay(30*time.Second).
Build())
if err != nil {
return nil, err
}

f.Client.Logger.Println("FTD upgrade successful.")
return ftdDevice, nil
}

Expand All @@ -70,49 +110,44 @@ func (f *ftdUpgradeService) validateConnectivityState(ftdDevice *FtdDevice) erro
return nil
}

func (f *ftdUpgradeService) validateFtdVersion(ftdDevice *FtdDevice, softwareVersionToUpgradeToStr string) error {
func (f *ftdUpgradeService) validateFtdVersion(ftdDevice *FtdDevice, softwareVersionToUpgradeToStr string) (*ftd.Version, 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
return nil, 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
return nil, err
}

if versionOnDevice.GreaterThan(versionToUpgradeTo) {
return errors.New(fmt.Sprintf("FTD device is on version %s, which is newer than the"+
return nil, 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 versionToUpgradeTo, nil
}

return nil
return nil, nil
}

func (f *ftdUpgradeService) validateUpgradePathExistsTo(ftdDevice *FtdDevice, toVersion *ftd.Version) error {
func (f *ftdUpgradeService) validateUpgradePathExistsTo(ftdDevice *FtdDevice, toVersion *ftd.Version) (*UpgradePackage, error) {
upgradePackages, err := ReadUpgradePackages(f.Ctx, *f.Client, ftdDevice.Uid)
if err != nil {
return err
return nil, 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
return nil, err
}
if softwareVersion.Equal(toVersion) {
return nil
return &upgradePackage, nil
}
}

return errors.New(fmt.Sprintf("%s is not a valid version to upgrade FTD device %s to", toVersion.String(), ftdDevice.Name))
return nil, errors.New(fmt.Sprintf("%s is not a valid version to upgrade FTD device %s to", toVersion.String(), ftdDevice.Name))
}
47 changes: 41 additions & 6 deletions client/device/cloudftd/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package cloudftd_test

import (
"errors"
"fmt"
"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/internal/publicapi/transaction"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi/transaction/transactionstatus"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi/transaction/transactiontype"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/model"
"github.com/google/uuid"
"github.com/jarcoal/httpmock"
Expand Down Expand Up @@ -164,6 +168,9 @@ func TestUpgrade(t *testing.T) {
assert.NotNil(t, ftdDevice)
assert.Nil(t, err)
assert.Equal(t, expectedFtdDevice, ftdDevice)
info := httpmock.GetCallCountInfo()
assert.Equal(t, 0, info[mockhttp.MethodGet+baseUrl+"/aegis/rest/v1/services/targets/devices/"+ftdDevice.Uid],
"Should not call the API to get the device's upgrade packages because the device's software version is equal to the version to upgrade to")
},
},
{
Expand Down Expand Up @@ -196,9 +203,8 @@ func TestUpgrade(t *testing.T) {
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",
testName: "Upgrade FTD device - Do not fail when the device is upgraded successfully",
uid: uuid.New().String(),
softwareVersion: "7.2.5.1-29",
expectedFtdDevice: &cloudftd.FtdDevice{
Expand All @@ -211,20 +217,49 @@ func TestUpgrade(t *testing.T) {
Tags: nil,
SoftwareVersion: "7.2.3",
},
expectedError: errors.New("upgrade implementation coming soon"),
expectedError: nil,
setupFunc: func(deviceUid string, softwareVersion string, ftdDevice *cloudftd.FtdDevice) {
transactionUid := uuid.New().String()
inProgressTransaction := transaction.Type{
TransactionUid: uuid.New().String(),
TenantUid: uuid.New().String(),
EntityUid: uuid.New().String(),
EntityUrl: baseUrl + "/api/rest/v1/inventory/devices/" + deviceUid,
PollingUrl: baseUrl + "/api/rest/v1/transactions/" + transactionUid,
SubmissionTime: "2025-09-07T20:10:00Z",
LastUpdatedTime: "2025-10-07T20:10:00Z",
Type: transactiontype.UPGRADE_FTD,
Status: transactionstatus.IN_PROGRESS,
}
doneTransaction := transaction.Type{
TransactionUid: inProgressTransaction.TransactionUid,
TenantUid: inProgressTransaction.TenantUid,
EntityUid: inProgressTransaction.EntityUid,
EntityUrl: inProgressTransaction.EntityUrl,
PollingUrl: inProgressTransaction.PollingUrl,
SubmissionTime: inProgressTransaction.SubmissionTime,
LastUpdatedTime: "2025-10-07T20:11:00Z",
Type: inProgressTransaction.Type,
Status: transactionstatus.DONE,
}
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),
}))
httpmock.RegisterResponder(mockhttp.MethodPost,
baseUrl+"/api/rest/v1/inventory/devices/ftds/"+ftdDevice.Uid+"/upgrades/trigger",
httpmock.NewJsonResponderOrPanic(202, inProgressTransaction))
httpmock.RegisterResponder(mockhttp.MethodGet,
fmt.Sprintf("%s/api/rest/v1/transactions/%s", baseUrl, transactionUid),
httpmock.NewJsonResponderOrPanic(200, doneTransaction))
},
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())
assert.NotNil(t, ftdDevice)
assert.Nil(t, err)
assert.Equal(t, expectedFtdDevice, ftdDevice)
},
},
}
Expand Down
2 changes: 1 addition & 1 deletion client/internal/publicapi/publicapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func untilDoneOrError(ctx context.Context, client http.Client, transactionPollin
return false, err
}
*trans = t
client.Logger.Printf("status=%s\n", t.Status)
client.Logger.Printf("Polled transaction:\ntransactionUid=%s,\nstatus=%s,\ndetails%s\n", t.TransactionUid, t.Status, t.Details)
return isDoneOrError(t), nil
}
}
Expand Down
1 change: 1 addition & 0 deletions client/internal/publicapi/transaction/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Type struct {
LastUpdatedTime string `json:"lastUpdatedTime"`
Type transactiontype.Type `json:"transactionType"`
Status transactionstatus.Type `json:"cdoTransactionStatus"`
Details map[string]string `json:"transactionDetails"`
ErrorMessage string `json:"errorMessage"`
ErrorDetails map[string]string `json:"errorDetails"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ const (
MSP_ADD_USER_GROUPS_TO_TENANT Type = "MSP_ADD_USER_GROUPS_TO_TENANT"
MSP_DELETE_USER_GROUPS_FROM_TENANT Type = "MSP_DELETE_USER_GROUPS_FROM_TENANT"
UPGRADE_ASA Type = "UPGRADE_ASA"
UPGRADE_FTD Type = "UPGRADE_FTD"
)
4 changes: 4 additions & 0 deletions client/internal/url/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,7 @@ func GetUpgradeAsaUrl(baseUrl string, deviceUid string) string {
func GetFtdUpgradePackagesUrl(baseUrl string, deviceUid string) string {
return fmt.Sprintf("%s/api/rest/v1/inventory/devices/ftds/%s/upgrades/versions", baseUrl, deviceUid)
}

func GetFtdUpgradeUrl(baseUrl string, deviceUid string) string {
return fmt.Sprintf("%s/api/rest/v1/inventory/devices/ftds/%s/upgrades/trigger", baseUrl, deviceUid)
}
2 changes: 1 addition & 1 deletion provider/examples/resources/user/example.tf
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
terraform {
required_providers {
cdo = {
source = "hashicorp.com/CiscoDevnet/cdo"
source = "CiscoDevnet/cdo"
}
}
}
Expand Down

0 comments on commit 63d1766

Please sign in to comment.