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,
 	}
 }