From 2ac82aa63ce1ebcf5fb3d3aad5e70ca6e79ca1f3 Mon Sep 17 00:00:00 2001 From: Christopher Dickson Date: Thu, 27 Jul 2023 15:51:43 +0100 Subject: [PATCH] LH68644- Feat: implement updating of asa device location (#12) * fix: implement updating of asa device location * refactor: abstract literals into constants * refactor: update location via usual update method * style: remove commented code * refactor: change if prefixed function to is --- client/client.go | 3 +- client/device/asa/read.go | 3 +- client/device/asa/update.go | 62 ++++++++++++------ client/internal/device/asaconfig/update.go | 55 ++++++++++++---- client/model/lar.go | 36 +++++++++++ provider/internal/device/asa/resource.go | 64 +++++++++++++------ provider/internal/device/asa/resource_test.go | 21 ++++++ 7 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 client/model/lar.go diff --git a/client/client.go b/client/client.go index 38e8714a..69f8680a 100644 --- a/client/client.go +++ b/client/client.go @@ -4,10 +4,11 @@ package client import ( "context" + "net/http" + "github.com/cisco-lockhart/go-client/connector/sdc" "github.com/cisco-lockhart/go-client/device/ios" "github.com/cisco-lockhart/go-client/internal/device/asaconfig" - "net/http" "github.com/cisco-lockhart/go-client/device/asa" internalhttp "github.com/cisco-lockhart/go-client/internal/http" diff --git a/client/device/asa/read.go b/client/device/asa/read.go index ca17e739..ced28aa2 100644 --- a/client/device/asa/read.go +++ b/client/device/asa/read.go @@ -20,7 +20,8 @@ type ReadOutput struct { Port string `json:"port"` Host string `json:"host"` - IgnoreCertifcate bool `json:"ignoreCertificate"` + IgnoreCertifcate bool `json:"ignoreCertificate"` + ConnectivityState int `json:"connectivityState"` } func NewReadInput(uid string) *ReadInput { diff --git a/client/device/asa/update.go b/client/device/asa/update.go index 67fe9c49..60b56232 100644 --- a/client/device/asa/update.go +++ b/client/device/asa/update.go @@ -7,6 +7,7 @@ import ( "github.com/cisco-lockhart/go-client/connector/sdc" "github.com/cisco-lockhart/go-client/internal/device/asaconfig" + "github.com/cisco-lockhart/go-client/internal/retry" "github.com/cisco-lockhart/go-client/device" "github.com/cisco-lockhart/go-client/internal/http" @@ -16,6 +17,7 @@ import ( type UpdateInput struct { Uid string `json:"-"` Name string `json:"name"` + Location string Username string Password string } @@ -44,7 +46,7 @@ func Update(ctx context.Context, client http.Client, updateInp UpdateInput) (*Up client.Logger.Println("updating asa device") - if updateInp.Username != "" { + if isSpecificDeviceIsRequired(updateInp) { asaReadSpecOutp, err := device.ReadSpecific(ctx, client, *device.NewReadSpecificInput( updateInp.Uid, @@ -60,32 +62,48 @@ func Update(ctx context.Context, client http.Client, updateInp UpdateInput) (*Up return nil, err } - var publicKey *sdc.PublicKey - if strings.EqualFold(asaReadOutp.LarType, "SDC") { - if asaReadOutp.LarUid == "" { - return nil, fmt.Errorf("sdc uid not found") + if updateInp.Username != "" || updateInp.Password != "" { + var publicKey *sdc.PublicKey + if strings.EqualFold(asaReadOutp.LarType, "SDC") { + if asaReadOutp.LarUid == "" { + return nil, fmt.Errorf("sdc uid not found") + } + + larReadRes, err := sdc.ReadByUid(ctx, client, sdc.ReadInput{ + LarUid: asaReadOutp.LarUid, + }) + if err != nil { + return nil, err + } + publicKey = &larReadRes.PublicKey } - larReadRes, err := sdc.ReadByUid(ctx, client, sdc.ReadInput{ - LarUid: asaReadOutp.LarUid, - }) + updateAsaConfigInp := asaconfig.NewUpdateInput( + asaReadSpecOutp.SpecificUid, + updateInp.Username, + updateInp.Password, + publicKey, + asaReadSpecOutp.State, + ) + _, err = asaconfig.UpdateCredentials(ctx, client, *updateAsaConfigInp) if err != nil { + _ = fmt.Errorf("failed to update credentials for ASA device: %s", err.Error()) return nil, err } - publicKey = &larReadRes.PublicKey } - updateAsaConfigInp := asaconfig.NewUpdateInput( - asaReadSpecOutp.SpecificUid, - updateInp.Username, - updateInp.Password, - publicKey, - asaReadSpecOutp.State, - ) - _, err = asaconfig.UpdateCredentials(ctx, client, *updateAsaConfigInp) - if err != nil { - _ = fmt.Errorf("failed to update credentials for ASA device: %s", err.Error()) - return nil, err + if updateInp.Location != "" { + _, err := asaconfig.UpdateLocation(ctx, client, asaconfig.UpdateLocationOptions{ + SpecificUid: asaReadSpecOutp.SpecificUid, + Location: updateInp.Location, + }) + if err != nil { + return nil, err + } + + if err := retry.Do(asaconfig.UntilStateDone(ctx, client, asaReadSpecOutp.SpecificUid), retry.DefaultOpts); err != nil { + return nil, err + } } } @@ -98,3 +116,7 @@ func Update(ctx context.Context, client http.Client, updateInp UpdateInput) (*Up return &outp, nil } + +func isSpecificDeviceIsRequired(updateInput UpdateInput) bool { + return updateInput.Username != "" || updateInput.Password != "" || updateInput.Location != "" +} diff --git a/client/internal/device/asaconfig/update.go b/client/internal/device/asaconfig/update.go index 66ffe931..4b68b591 100644 --- a/client/internal/device/asaconfig/update.go +++ b/client/internal/device/asaconfig/update.go @@ -78,6 +78,39 @@ func UpdateCredentials(ctx context.Context, client http.Client, updateInput Upda return &outp, nil } +type UpdateLocationOptions struct { + SpecificUid string + Location string +} + +type updateLocationRequestBody struct { + QueueTriggerState string `json:"queueTriggerState"` + SmContext pendingLocationUpdateSmContext `json:"stateMachineContext"` +} + +type pendingLocationUpdateSmContext struct { + Ipv4 string `json:"ipv4"` +} + +func UpdateLocation(ctx context.Context, client http.Client, options UpdateLocationOptions) (*UpdateOutput, error) { + url := url.UpdateAsaConfig(client.BaseUrl(), options.SpecificUid) + + req := client.NewPut(ctx, url, updateLocationRequestBody{ + QueueTriggerState: "PENDING_LOCATION_UPDATE", + SmContext: pendingLocationUpdateSmContext{ + options.Location, + }, + }) + + var outp UpdateOutput + err := req.Send(&outp) + if err != nil { + return nil, err + } + + return &outp, nil +} + func makeReqBody(creds []byte) *updateBody { return &updateBody{ State: "CERT_VALIDATED", // question: should this be hardcoded? @@ -144,22 +177,20 @@ func encrypt(req *UpdateInput) error { } func makeCredentials(updateInp UpdateInput) ([]byte, error) { - var creds []byte - var err error if updateInp.PublicKey != nil { - err = encrypt(&updateInp) - if err == nil { - creds, err = json.Marshal(credentials{ - Username: updateInp.Username, - Password: updateInp.Password, - KeyId: updateInp.PublicKey.KeyId, - }) + if err := encrypt(&updateInp); err != nil { + return nil, err } - } else { - creds, err = json.Marshal(credentials{ + + return json.Marshal(credentials{ Username: updateInp.Username, Password: updateInp.Password, + KeyId: updateInp.PublicKey.KeyId, }) } - return creds, err + + return json.Marshal(credentials{ + Username: updateInp.Username, + Password: updateInp.Password, + }) } diff --git a/client/model/lar.go b/client/model/lar.go new file mode 100644 index 00000000..3b4faa20 --- /dev/null +++ b/client/model/lar.go @@ -0,0 +1,36 @@ +package model + +import ( + "fmt" + "strings" +) + +type LarType string + +const ( + LarTypeCloudDeviceGateway LarType = "CDG" + LarTypeSecureDeviceConnector LarType = "SDC" +) + +func ParseLarType(input string) (LarType, error) { + switch strings.ToUpper(input) { + case string(LarTypeCloudDeviceGateway): + return LarTypeCloudDeviceGateway, nil + + case string(LarTypeSecureDeviceConnector): + return LarTypeSecureDeviceConnector, nil + + default: + return "", fmt.Errorf("'%s' is not a valid lar type", input) + + } +} + +func MustParseLarType(input string) LarType { + larType, err := ParseLarType(input) + if err != nil { + panic(err) + } + + return larType +} diff --git a/provider/internal/device/asa/resource.go b/provider/internal/device/asa/resource.go index 66921bec..c25a18a2 100644 --- a/provider/internal/device/asa/resource.go +++ b/provider/internal/device/asa/resource.go @@ -246,21 +246,19 @@ func (r *AsaDeviceResource) Update(ctx context.Context, req resource.UpdateReque return } - var updateInp asa.UpdateInput + updateInp := asa.UpdateInput{Uid: stateData.ID.ValueString(), Name: stateData.Name.ValueString()} + + if isNameUpdated(planData, stateData) { + updateInp.Name = planData.Name.ValueString() + } + + if isLocationUpdated(planData, stateData) { + updateInp.Location = planData.Ipv4.ValueString() + } + if isCredentialUpdated(planData, stateData) { - updateInp = *asa.NewUpdateInput( - stateData.ID.ValueString(), - planData.Name.ValueString(), - planData.Username.ValueString(), - planData.Password.ValueString(), - ) - } else { - updateInp = *asa.NewUpdateInput( - stateData.ID.ValueString(), - planData.Name.ValueString(), - "", - "", - ) + updateInp.Username = planData.Username.ValueString() + updateInp.Password = planData.Password.ValueString() } updateOutp, err := r.client.UpdateAsa(ctx, updateInp) @@ -269,17 +267,26 @@ func (r *AsaDeviceResource) Update(ctx context.Context, req resource.UpdateReque return } + port, err := parsePort(updateOutp.Port) + if err != nil { + res.Diagnostics.AddError("unable to parse port", err.Error()) + return + } + + stateData.ID = types.StringValue(updateOutp.Uid) + stateData.SdcType = types.StringValue(planData.SdcType.ValueString()) + stateData.SdcName = types.StringValue(planData.SdcName.ValueString()) stateData.Name = types.StringValue(updateOutp.Name) - stateData.Username = types.StringValue(planData.Username.ValueString()) - stateData.Password = types.StringValue(planData.Password.ValueString()) + stateData.Ipv4 = types.StringValue(updateOutp.Ipv4) + stateData.Host = types.StringValue(updateOutp.Host) + stateData.Port = types.Int64Value(port) + stateData.Username = planData.Username + stateData.Password = planData.Password + stateData.IgnoreCertifcate = planData.IgnoreCertifcate res.Diagnostics.Append(res.State.Set(ctx, &stateData)...) } -func isCredentialUpdated(planData *AsaDeviceResourceModel, stateData *AsaDeviceResourceModel) bool { - return planData.Username.ValueString() != stateData.Username.ValueString() || planData.Password.ValueString() != stateData.Password.ValueString() -} - func (r *AsaDeviceResource) Delete(ctx context.Context, req resource.DeleteRequest, res *resource.DeleteResponse) { tflog.Trace(ctx, "delete ASA device resource") @@ -303,3 +310,20 @@ func (r *AsaDeviceResource) Delete(ctx context.Context, req resource.DeleteReque func (r *AsaDeviceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, res *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, res) } + +func isCredentialUpdated(planData, stateData *AsaDeviceResourceModel) bool { + return planData.Username.ValueString() != stateData.Username.ValueString() || planData.Password.ValueString() != stateData.Password.ValueString() +} + +func isNameUpdated(planData, stateData *AsaDeviceResourceModel) bool { + return !planData.Name.Equal(stateData.Name) +} + +func isLocationUpdated(planData, stateData *AsaDeviceResourceModel) bool { + return !planData.Ipv4.Equal(stateData.Ipv4) +} + +func parsePort(rawPort string) (int64, error) { + return strconv.ParseInt(rawPort, 10, 16) + +} diff --git a/provider/internal/device/asa/resource_test.go b/provider/internal/device/asa/resource_test.go index 0ce0ff66..558b201a 100644 --- a/provider/internal/device/asa/resource_test.go +++ b/provider/internal/device/asa/resource_test.go @@ -46,6 +46,9 @@ var testAsaResource_SDC = testAsaResourceType{ Host: "vasa-gb-ravpn-03-mgmt.dev.lockhart.io", Port: "443", } + +const alternativeDeviceLocation = "35.177.20.218:443" + var testAsaResourceConfig_SDC = acctest.MustParseTemplate(asaResourceTemplate, testAsaResource_SDC) // new name config. @@ -60,6 +63,9 @@ var testAsaResource_SDC_NewCreds = acctest.MustOverrideFields(testAsaResource_SD }) var testAsaResourceConfig_SDC_NewCreds = acctest.MustParseTemplate(asaResourceTemplate, testAsaResource_SDC_NewCreds) +var testAsaResource_SDC_NewLocation = acctest.MustOverrideFields(testAsaResource_SDC, map[string]any{"Ipv4": alternativeDeviceLocation}) +var testAsaResourceConfig_SDC_NewLocation = acctest.MustParseTemplate(asaResourceTemplate, testAsaResource_SDC_NewLocation) + // CDG configs // default config. @@ -89,6 +95,9 @@ var testAsaResource_CDG_NewCreds = acctest.MustOverrideFields(testAsaResource_CD }) var testAsaResourceConfig_CDG_NewCreds = acctest.MustParseTemplate(asaResourceTemplate, testAsaResource_CDG_NewCreds) +var testAsaResource_CDG_NewLocation = acctest.MustOverrideFields(testAsaResource_CDG, map[string]any{"Ipv4": alternativeDeviceLocation}) +var testAsaResourceConfig_CDG_NewLocation = acctest.MustParseTemplate(asaResourceTemplate, testAsaResource_CDG_NewLocation) + func TestAccAsaDeviceResource_SDC(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: acctest.PreCheckFunc(t), @@ -128,6 +137,12 @@ func TestAccAsaDeviceResource_SDC(t *testing.T) { resource.TestCheckResourceAttr("cdo_asa_device.test", "password", testAsaResource_SDC.Password), ), }, + { + Config: acctest.ProviderConfig() + testAsaResourceConfig_SDC_NewLocation, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("cdo_asa_device.test", "ipv4", testAsaResource_SDC_NewLocation.Ipv4), + ), + }, // Delete testing automatically occurs in TestCase }, }) @@ -172,6 +187,12 @@ func TestAccAsaDeviceResource_CDG(t *testing.T) { resource.TestCheckResourceAttr("cdo_asa_device.test", "password", testAsaResource_CDG.Password), ), }, + { + Config: acctest.ProviderConfig() + testAsaResourceConfig_CDG_NewLocation, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("cdo_asa_device.test", "ipv4", testAsaResource_CDG_NewLocation.Ipv4), + ), + }, // Delete testing automatically occurs in TestCase }, })