diff --git a/README.md b/README.md index e7e9a4d..3b4319f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ resources from Terraform provider plugin. Library gives possibility to manage virtual network devices and associated network services. -**NOTE**: scope of this library is limited to needs of Terraform provider plugin +**NOTE**: Scope of this library is limited to needs of Terraform provider plugin and it is not providing full capabilities of Equinix Network Edge API ## Usage diff --git a/client.go b/client.go index 180b7f0..71c4788 100644 --- a/client.go +++ b/client.go @@ -15,6 +15,10 @@ const ( DeviceStateWaitingPrimary = "WAITING_FOR_PRIMARY" //DeviceStateWaitingSecondary primary Network Edge device is waiting for provisioning of its redundant (secondary) device DeviceStateWaitingSecondary = "WAITING_FOR_SECONDARY" + //DeviceStateWaitingClusterNodes primary Network Edge device is waiting for provisioning of its cluster devices + DeviceStateWaitingClusterNodes = "WAITING_FOR_REPLICA_CLUSTER_NODES" + //DeviceStateClusterSetUpInProgress The cluster setup is in progress + DeviceStateClusterSetUpInProgress = "CLUSTER_SETUP_IN_PROGRESS" //DeviceStateFailed Network Edge device creation and provisioning have failed DeviceStateFailed = "FAILED" //DeviceStateProvisioned Network Edge device was successfully provisioned and is fully operational @@ -32,6 +36,8 @@ const ( DeviceLicenseStateApplied = "APPLIED" //DeviceLicenseStateFailed license registration has failed DeviceLicenseStateFailed = "REGISTRATION_FAILED" + //DeviceLicenseStateWaitingClusterSetUp license is waiting for cluster setup + DeviceLicenseStateWaitingClusterSetUp = "WAITING_FOR_CLUSTER_SETUP" //BGPStateIdle BGP peer state is idle BGPStateIdle = "Idle" @@ -239,6 +245,7 @@ type Device struct { LicenseFile *string LicenseFileID *string ACLTemplateUUID *string + MgmtAclTemplateUuid *string SSHIPAddress *string SSHIPFqdn *string AccountNumber *string @@ -258,6 +265,7 @@ type Device struct { UserPublicKey *DeviceUserPublicKey ASN *int ZoneCode *string + ClusterDetails *ClusterDetails } //DeviceInterface describes Network Edge device interface @@ -417,3 +425,27 @@ type DeviceLinkGroupLink struct { SourceZoneCode *string DestinationZoneCode *string } + +//ClusterDetails describes Network Edge cluster device details +type ClusterDetails struct { + ClusterName *string + NumOfNodes *int + ClusterNodeDetails map[string]*ClusterNodeDetail + ClusterId *string + Nodes []ClusterNode +} + +//ClusterNodeDetail describes Network Edge cluster node configuration +type ClusterNodeDetail struct { + VendorConfiguration map[string]string + LicenseFileId *string + LicenseToken *string +} + +type ClusterNode struct { + UUID *string + Name *string + Node *int + AdminPassword *string + VendorConfiguration map[string]string +} diff --git a/go.sum b/go.sum index 8487c39..fdefdf7 100644 --- a/go.sum +++ b/go.sum @@ -4,15 +4,12 @@ github.com/equinix/rest-go v1.3.0 h1:m38scYTOfV6N+gcrwchgVDutDffYd+QoYCMm9Jn6jyk github.com/equinix/rest-go v1.3.0/go.mod h1:7pjEgOdG2MZO9BGkQzSurSgVQxRfzc1enceXJS6hYDw= github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= -github.com/jarcoal/httpmock v1.0.6 h1:e81vOSexXU3mJuJ4l//geOmKIt+Vkxerk1feQBC8D0g= github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k= github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= 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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/api/device.go b/internal/api/device.go index 10ed09e..6951d11 100644 --- a/internal/api/device.go +++ b/internal/api/device.go @@ -19,6 +19,7 @@ type Device struct { LicenseType *string `json:"licenseType,omitempty"` LicenseFileID *string `json:"licenseFileId,omitempty"` ACLTemplateUUID *string `json:"aclTemplateUuid,omitempty"` + MgmtAclTemplateUuid *string `json:"mgmtAclTemplateUuid,omitempty"` SSHIPAddress *string `json:"sshIpAddress,omitempty"` SSHIPFqdn *string `json:"sshIpFqdn,omitempty"` AccountNumber *string `json:"accountNumber,omitempty"` @@ -38,6 +39,7 @@ type Device struct { UserPublicKey *DeviceUserPublicKey `json:"userPublicKey,omitempty"` ASN *int `json:"asn,omitempty"` ZoneCode *string `json:"zoneCode,omitempty"` + ClusterDetails *ClusterDetails `json:"clusterDetails,omitempty"` } //DeviceRequest describes network edge device creation request @@ -64,9 +66,11 @@ type DeviceRequest struct { Core *int `json:"core,omitempty"` AdditionalBandwidth *int `json:"additionalBandwidth,omitempty,string"` ACLTemplateUUID *string `json:"aclTemplateUuid,omitempty"` + MgmtAclTemplateUuid *string `json:"mgmtAclTemplateUuid,omitempty"` VendorConfig map[string]string `json:"vendorConfig,omitempty"` UserPublicKey *DeviceUserPublicKeyRequest `json:"userPublicKey,omitempty"` Secondary *SecondaryDeviceRequest `json:"secondary,omitempty"` + ClusterDetails *ClusterDetailsRequest `json:"clusterDetails,omitempty"` } //SecondaryDeviceRequest describes secondary device part of device creation request @@ -122,8 +126,10 @@ type DeviceCoreInformation struct { //DeviceRequestResponse describes response for device creation request type DeviceRequestResponse struct { - UUID *string `json:"uuid,omitempty"` - SecondaryUUID *string `json:"secondaryUUID,omitempty"` + UUID *string `json:"uuid,omitempty"` + SecondaryUUID *string `json:"secondaryUuid,omitempty"` + DeviceIDs map[string]string `json:"deviceIds,omitempty"` + ClusterID *string `json:"clusterId,omitempty"` } //DeviceUpdateRequest describes network device update request @@ -159,3 +165,34 @@ type DeviceAdditionalBandwidthResponse struct { type DeviceACLResponse struct { Status *string `json:"status,omitempty"` } + +//ClusterDetailsRequest describes cluster details of device creation request +type ClusterDetailsRequest struct { + ClusterName *string `json:"clusterName,omitempty"` + NumOfNodes *int `json:"numOfNodes,omitempty"` + ClusterNodeDetails map[string]ClusterNodeDetailRequest `json:"clusterNodeDetails,omitempty"` +} + +//ClusterNodeDetailRequest describes cluster node configuration of device creation request +type ClusterNodeDetailRequest struct { + VendorConfiguration map[string]string `json:"vendorConfig,omitempty"` + LicenseFileId *string `json:"licenseFileId,omitempty"` + LicenseToken *string `json:"licenseToken,omitempty"` +} + +//ClusterDetails describes cluster details for device response +type ClusterDetails struct { + ClusterId *string `json:"clusterId,omitempty"` + ClusterName *string `json:"clusterName,omitempty"` + NumOfNodes *int `json:"numOfNodes,omitempty"` + Nodes []ClusterNode `json:"nodes,omitempty"` +} + +//ClusterNode describes cluster node details for device response +type ClusterNode struct { + UUID *string `json:"uuid,omitempty"` + Name *string `json:"name,omitempty"` + Node *int `json:"node,omitempty"` + AdminPassword *string `json:"adminPwd,omitempty"` + VendorConfiguration map[string]string `json:"vendorConfig,omitempty"` +} diff --git a/rest_device.go b/rest_device.go index 4e13faf..a3c2dc3 100644 --- a/rest_device.go +++ b/rest_device.go @@ -209,6 +209,7 @@ func mapDeviceAPIToDomain(apiDevice api.Device) *Device { device.LicenseToken = apiDevice.LicenseToken device.LicenseFileID = apiDevice.LicenseFileID device.ACLTemplateUUID = apiDevice.ACLTemplateUUID + device.MgmtAclTemplateUuid = apiDevice.MgmtAclTemplateUuid device.SSHIPAddress = apiDevice.SSHIPAddress device.SSHIPFqdn = apiDevice.SSHIPFqdn device.AccountNumber = apiDevice.AccountNumber @@ -236,6 +237,7 @@ func mapDeviceAPIToDomain(apiDevice api.Device) *Device { device.UserPublicKey = mapDeviceUserPublicKeyAPIToDomain(apiDevice.UserPublicKey) device.ASN = apiDevice.ASN device.ZoneCode = apiDevice.ZoneCode + device.ClusterDetails = mapDeviceClusterDetailsAPIToDomain(apiDevice.ClusterDetails) return &device } @@ -276,6 +278,55 @@ func mapDeviceUserPublicKeyDomainToAPI(userKey *DeviceUserPublicKey) *api.Device } } +func mapDeviceClusterDetailsAPIToDomain(apiClusterDetails *api.ClusterDetails) *ClusterDetails { + if apiClusterDetails == nil { + return nil + } + clusterDetails := ClusterDetails{} + clusterDetails.ClusterId = apiClusterDetails.ClusterId + clusterDetails.ClusterName = apiClusterDetails.ClusterName + clusterDetails.NumOfNodes = apiClusterDetails.NumOfNodes + apiNodes := apiClusterDetails.Nodes + transformed := make([]ClusterNode, len(apiNodes)) + for i := range apiNodes { + transformed[i] = ClusterNode{ + UUID: apiNodes[i].UUID, + Name: apiNodes[i].Name, + Node: apiNodes[i].Node, + AdminPassword: apiNodes[i].AdminPassword, + VendorConfiguration: apiNodes[i].VendorConfiguration, + } + } + clusterDetails.Nodes = transformed + return &clusterDetails +} + +func mapDeviceClusterDetailsDomainToAPI(clusterDetails *ClusterDetails) *api.ClusterDetailsRequest { + if clusterDetails == nil { + return nil + } + req := api.ClusterDetailsRequest{} + req.ClusterName = clusterDetails.ClusterName + req.NumOfNodes = clusterDetails.NumOfNodes + clusterNodeDetailsRequest := make(map[string]api.ClusterNodeDetailRequest) + for k, v := range clusterDetails.ClusterNodeDetails { + clusterNodeDetailsRequest[k] = *mapDeviceClusterNodeDetailDomainToAPI(v) + } + req.ClusterNodeDetails = clusterNodeDetailsRequest + return &req +} + +func mapDeviceClusterNodeDetailDomainToAPI(clusterNodeDetail *ClusterNodeDetail) *api.ClusterNodeDetailRequest { + if clusterNodeDetail == nil { + return nil + } + return &api.ClusterNodeDetailRequest{ + VendorConfiguration: clusterNodeDetail.VendorConfiguration, + LicenseFileId: clusterNodeDetail.LicenseFileId, + LicenseToken: clusterNodeDetail.LicenseToken, + } +} + func createDeviceRequest(device Device) api.DeviceRequest { req := api.DeviceRequest{} req.Throughput = device.Throughput @@ -315,8 +366,10 @@ func createDeviceRequest(device Device) api.DeviceRequest { req.AdditionalBandwidth = device.AdditionalBandwidth req.SshInterfaceId = device.WanInterfaceId req.ACLTemplateUUID = device.ACLTemplateUUID + req.MgmtAclTemplateUuid = device.MgmtAclTemplateUuid req.VendorConfig = device.VendorConfiguration req.UserPublicKey = mapDeviceUserPublicKeyDomainToAPI(device.UserPublicKey) + req.ClusterDetails = mapDeviceClusterDetailsDomainToAPI(device.ClusterDetails) return req } diff --git a/rest_device_test.go b/rest_device_test.go index 8b98098..3c24c54 100644 --- a/rest_device_test.go +++ b/rest_device_test.go @@ -127,6 +127,76 @@ func TestCreateRedundantDevice(t *testing.T) { verifyRedundantDeviceRequest(t, primary, secondary, req) } +func TestCreateClusterDevice(t *testing.T) { + //given + resp := api.DeviceRequestResponse{} + if err := readJSONData("./test-fixtures/ne_device_create_resp.json", &resp); err != nil { + assert.Fail(t, "Cannot read test response") + } + device := Device{ + Name: String("PANW-cluster"), + MetroCode: String("SV"), + TypeCode: String("PA-VM"), + IsSelfManaged: Bool(true), + IsBYOL: Bool(true), + PackageCode: String("VM100"), + Notifications: []string{"[email protected]", "[email protected]"}, + HostName: String("panwHostName"), + TermLength: Int(24), + AccountNumber: String("177643"), + Version: String("10.1.3"), + InterfaceCount: Int(10), + CoreCount: Int(2), + ACLTemplateUUID: String("4972e8d2-417f-4821-91a8-f4a61a6dcdc3"), + MgmtAclTemplateUuid: String("4972e8d2-417f-4821-91a8-f4a61a6dcdc3"), + UserPublicKey: &DeviceUserPublicKey{ + Username: String("testUserName"), + KeyName: String("testKey"), + }, + ClusterDetails: &ClusterDetails{ + ClusterName: String("clusterName"), + ClusterNodeDetails: map[string]*ClusterNodeDetail{ + "node0": { + VendorConfiguration: map[string]string{ + "hostName": "panw-host0", + }, + LicenseFileId: String("8d180057-8309-4c59-b645-f630f010ad43"), + LicenseToken: String("licenseToken"), + }, + "node1": { + VendorConfiguration: map[string]string{ + "hostName": "panw-host1", + }, + LicenseFileId: String("8d180057-8309-4c59-b645-f630f010ad43"), + LicenseToken: String("licenseToken"), + }, + }, + }, + } + req := api.DeviceRequest{} + testHc := &http.Client{} + httpmock.ActivateNonDefault(testHc) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/ne/v1/devices", baseURL), + func(r *http.Request) (*http.Response, error) { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return httpmock.NewStringResponse(400, ""), nil + } + resp, _ := httpmock.NewJsonResponse(202, resp) + return resp, nil + }, + ) + defer httpmock.DeactivateAndReset() + + //when + c := NewClient(context.Background(), baseURL, testHc) + uuid, err := c.CreateDevice(device) + + //then + assert.Nil(t, err, "Error is not returned") + assert.Equal(t, uuid, resp.UUID, "UUID matches") + verifyClusterDeviceRequest(t, device, req) +} + func TestGetDevice(t *testing.T) { //given resp := api.Device{} @@ -413,6 +483,37 @@ func verifyRedundantDeviceRequest(t *testing.T, primary, secondary Device, req a verifyDeviceUserPublicKeyRequest(t, *secondary.UserPublicKey, *req.Secondary.UserPublicKey) } +func verifyClusterDeviceRequest(t *testing.T, device Device, req api.DeviceRequest) { + assert.Equal(t, device.Name, req.VirtualDeviceName, "Name matches") + assert.Equal(t, device.MetroCode, req.MetroCode, "MetroCode matches") + assert.Equal(t, device.TypeCode, req.DeviceTypeCode, "TypeCode matches") + if *device.IsSelfManaged { + assert.Equal(t, DeviceManagementTypeSelf, StringValue(req.DeviceManagementType), "DeviceManagementType matches") + } else { + assert.Equal(t, DeviceManagementTypeEquinix, StringValue(req.DeviceManagementType), "DeviceManagementType matches") + } + if *device.IsBYOL { + assert.Equal(t, DeviceLicenseModeBYOL, StringValue(req.LicenseMode), "LicenseMode matches") + } else { + assert.Equal(t, DeviceLicenseModeSubscription, StringValue(req.LicenseMode), "LicenseMode matches") + } + assert.Equal(t, device.PackageCode, req.PackageCode, "PackageCode matches") + assert.ElementsMatch(t, device.Notifications, req.Notifications, "Notifications matches") + assert.Equal(t, device.HostName, req.HostNamePrefix, "HostName matches") + termLengthStr := strconv.Itoa(*device.TermLength) + assert.Equal(t, &termLengthStr, req.TermLength, "TermLength matches") + assert.Equal(t, device.AccountNumber, req.AccountNumber, "AccountNumber matches") + assert.Equal(t, device.Version, req.Version, "Version matches") + assert.Equal(t, device.InterfaceCount, req.InterfaceCount, "InterfaceCount matches") + assert.Equal(t, device.CoreCount, req.Core, "Core matches") + assert.Equal(t, device.ACLTemplateUUID, req.ACLTemplateUUID, "ACLTemplateUUID matches") + assert.Equal(t, device.MgmtAclTemplateUuid, req.MgmtAclTemplateUuid, "MgmtAclTemplateUuid matches") + assert.NotNil(t, req.UserPublicKey, "UserPublicKey is not nil") + verifyDeviceUserPublicKeyRequest(t, *device.UserPublicKey, *req.UserPublicKey) + assert.NotNil(t, req.ClusterDetails, "ClusterDetails are not nil") + verifyClusterDetailsRequest(t, *device.ClusterDetails, *req.ClusterDetails) +} + func verifyDeviceUserPublicKey(t *testing.T, userKey DeviceUserPublicKey, apiUserKey api.DeviceUserPublicKey) { assert.Equal(t, apiUserKey.Username, userKey.Username, "Username matches") assert.Equal(t, apiUserKey.KeyName, userKey.KeyName, "KeyName matches") @@ -422,3 +523,19 @@ func verifyDeviceUserPublicKeyRequest(t *testing.T, userKey DeviceUserPublicKey, assert.Equal(t, apiUserKeyReq.Username, userKey.Username, "Username matches") assert.Equal(t, apiUserKeyReq.KeyName, userKey.KeyName, "KeyName matches") } + +func verifyClusterDetailsRequest(t *testing.T, clusterDetails ClusterDetails, apiClusterDetailsReq api.ClusterDetailsRequest) { + assert.Equal(t, clusterDetails.ClusterName, apiClusterDetailsReq.ClusterName, "ClusterName matches") + apiClusterNodeDetailReqMap := apiClusterDetailsReq.ClusterNodeDetails + assert.NotNil(t, apiClusterNodeDetailReqMap, "ClusterNodeDetails are not nil") + for k, v := range clusterDetails.ClusterNodeDetails { + verifyClusterNodeDetailRequest(t, v, apiClusterNodeDetailReqMap[k]) + } +} + +func verifyClusterNodeDetailRequest(t *testing.T, clusterNodeDetail *ClusterNodeDetail, apiClusterNodeDetailReq api.ClusterNodeDetailRequest) { + assert.NotNil(t, apiClusterNodeDetailReq, "ClusterNodeDetailRequest is not nil") + assert.Equal(t, clusterNodeDetail.VendorConfiguration, apiClusterNodeDetailReq.VendorConfiguration, "VendorConfigurations match") + assert.Equal(t, clusterNodeDetail.LicenseFileId, apiClusterNodeDetailReq.LicenseFileId, "LicenseFileId matches") + assert.Equal(t, clusterNodeDetail.LicenseToken, apiClusterNodeDetailReq.LicenseToken, "LicenseToken matches") +} diff --git a/test-fixtures/ne_device_create_resp.json b/test-fixtures/ne_device_create_resp.json index f157acd..9622aca 100644 --- a/test-fixtures/ne_device_create_resp.json +++ b/test-fixtures/ne_device_create_resp.json @@ -1,4 +1,4 @@ { "uuid": "877a3aa2-c49a-4af1-98a6-007424e737ae", - "secondaryUUID": "b22c289a-162a-4e51-879d-b3e779805fb7" + "secondaryUuid": "b22c289a-162a-4e51-879d-b3e779805fb7" } \ No newline at end of file