diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go index 92645c863..cd4d471a6 100644 --- a/acceptance/clients/clients.go +++ b/acceptance/clients/clients.go @@ -8,6 +8,8 @@ import ( "os" "strings" + "net/http" + "github.com/huaweicloud/golangsdk" "github.com/huaweicloud/golangsdk/openstack" ) @@ -122,6 +124,8 @@ func NewBlockStorageV1Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewBlockStorageV1(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -141,6 +145,8 @@ func NewBlockStorageV2Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewBlockStorageV2(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -160,6 +166,8 @@ func NewBlockStorageV3Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewBlockStorageV3(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -179,6 +187,8 @@ func NewComputeV2Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewComputeV2(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -198,6 +208,8 @@ func NewDBV1Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewDBV1(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -217,6 +229,8 @@ func NewDNSV2Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewDNSV2(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -236,6 +250,8 @@ func NewIdentityV2Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewIdentityV2(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -255,6 +271,8 @@ func NewIdentityV2AdminClient() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewIdentityV2(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), Availability: golangsdk.AvailabilityAdmin, @@ -275,6 +293,8 @@ func NewIdentityV2UnauthenticatedClient() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewIdentityV2(client, golangsdk.EndpointOpts{}) } @@ -292,6 +312,8 @@ func NewIdentityV3Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewIdentityV3(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -311,6 +333,8 @@ func NewIdentityV3UnauthenticatedClient() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewIdentityV3(client, golangsdk.EndpointOpts{}) } @@ -328,6 +352,8 @@ func NewImageServiceV2Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewImageServiceV2(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -347,6 +373,8 @@ func NewNetworkV1Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewNetworkV1(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -371,6 +399,8 @@ func NewPeerNetworkV1Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewNetworkV1(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -390,6 +420,8 @@ func NewNetworkV2Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewNetworkV2(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -414,6 +446,8 @@ func NewPeerNetworkV2Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewNetworkV2(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -433,6 +467,8 @@ func NewObjectStorageV1Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewObjectStorageV1(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -452,6 +488,8 @@ func NewSharedFileSystemV2Client() (*golangsdk.ServiceClient, error) { return nil, err } + configureDebug(client) + return openstack.NewSharedFileSystemV2(client, golangsdk.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -473,3 +511,15 @@ func UpdatePeerTenantDetails(ao *golangsdk.AuthOptions) error { return fmt.Errorf("You're missing some important setup:\n OS_Peer_Tenant_ID or OS_Peer_Tenant_Name env variables must be provided.") } } + +// configureDebug will configure the provider client to print the API +// requests and responses if OS_DEBUG is enabled. +func configureDebug(client *golangsdk.ProviderClient) { + if os.Getenv("OS_DEBUG") != "" { + client.HTTPClient = http.Client{ + Transport: &LogRoundTripper{ + Rt: &http.Transport{}, + }, + } + } +} diff --git a/acceptance/clients/conditions.go b/acceptance/clients/conditions.go new file mode 100644 index 000000000..7bf3f2626 --- /dev/null +++ b/acceptance/clients/conditions.go @@ -0,0 +1,60 @@ +package clients + +import ( + "os" + "testing" +) + +// RequireAdmin will restrict a test to only be run by admin users. +func RequireAdmin(t *testing.T) { + if os.Getenv("OS_USERNAME") != "admin" { + t.Skip("must be admin to run this test") + } +} + +// RequireDNS will restrict a test to only be run in environments +// that support DNSaaS. +func RequireDNS(t *testing.T) { + if os.Getenv("OS_DNS_ENVIRONMENT") == "" { + t.Skip("this test requires DNSaaS") + } +} + +// RequireGuestAgent will restrict a test to only be run in +// environments that support the QEMU guest agent. +func RequireGuestAgent(t *testing.T) { + if os.Getenv("OS_GUEST_AGENT") == "" { + t.Skip("this test requires support for qemu guest agent and to set OS_GUEST_AGENT to 1") + } +} + +// RequireIdentityV2 will restrict a test to only be run in +// environments that support the Identity V2 API. +func RequireIdentityV2(t *testing.T) { + if os.Getenv("OS_IDENTITY_API_VERSION") != "2.0" { + t.Skip("this test requires support for the identity v2 API") + } +} + +// RequireLiveMigration will restrict a test to only be run in +// environments that support live migration. +func RequireLiveMigration(t *testing.T) { + if os.Getenv("OS_LIVE_MIGRATE") == "" { + t.Skip("this test requires support for live migration and to set OS_LIVE_MIGRATE to 1") + } +} + +// RequireLong will ensure long-running tests can run. +func RequireLong(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } +} + +// RequireNovaNetwork will restrict a test to only be run in +// environments that support nova-network. +func RequireNovaNetwork(t *testing.T) { + if os.Getenv("OS_NOVANET") == "" { + t.Skip("this test requires nova-network and to set OS_NOVANET to 1") + } +} diff --git a/acceptance/clients/http.go b/acceptance/clients/http.go new file mode 100644 index 000000000..3f42231e3 --- /dev/null +++ b/acceptance/clients/http.go @@ -0,0 +1,170 @@ +package clients + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "sort" + "strings" +) + +// List of headers that need to be redacted +var REDACT_HEADERS = []string{"x-auth-token", "x-auth-key", "x-service-token", + "x-storage-token", "x-account-meta-temp-url-key", "x-account-meta-temp-url-key-2", + "x-container-meta-temp-url-key", "x-container-meta-temp-url-key-2", "set-cookie", + "x-subject-token"} + +// LogRoundTripper satisfies the http.RoundTripper interface and is used to +// customize the default http client RoundTripper to allow logging. +type LogRoundTripper struct { + Rt http.RoundTripper +} + +// RoundTrip performs a round-trip HTTP request and logs relevant information +// about it. +func (lrt *LogRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + defer func() { + if request.Body != nil { + request.Body.Close() + } + }() + + var err error + + log.Printf("[DEBUG] OpenStack Request URL: %s %s", request.Method, request.URL) + log.Printf("[DEBUG] OpenStack request Headers:\n%s", formatHeaders(request.Header)) + + if request.Body != nil { + request.Body, err = lrt.logRequest(request.Body, request.Header.Get("Content-Type")) + if err != nil { + return nil, err + } + } + + response, err := lrt.Rt.RoundTrip(request) + if response == nil { + return nil, err + } + + log.Printf("[DEBUG] OpenStack Response Code: %d", response.StatusCode) + log.Printf("[DEBUG] OpenStack Response Headers:\n%s", formatHeaders(response.Header)) + + response.Body, err = lrt.logResponse(response.Body, response.Header.Get("Content-Type")) + + return response, err +} + +// logRequest will log the HTTP Request details. +// If the body is JSON, it will attempt to be pretty-formatted. +func (lrt *LogRoundTripper) logRequest(original io.ReadCloser, contentType string) (io.ReadCloser, error) { + defer original.Close() + + var bs bytes.Buffer + _, err := io.Copy(&bs, original) + if err != nil { + return nil, err + } + + // Handle request contentType + if strings.HasPrefix(contentType, "application/json") { + debugInfo := lrt.formatJSON(bs.Bytes()) + log.Printf("[DEBUG] OpenStack Request Body: %s", debugInfo) + } else { + log.Printf("[DEBUG] OpenStack Request Body: %s", bs.String()) + } + + return ioutil.NopCloser(strings.NewReader(bs.String())), nil +} + +// logResponse will log the HTTP Response details. +// If the body is JSON, it will attempt to be pretty-formatted. +func (lrt *LogRoundTripper) logResponse(original io.ReadCloser, contentType string) (io.ReadCloser, error) { + if strings.HasPrefix(contentType, "application/json") { + var bs bytes.Buffer + defer original.Close() + _, err := io.Copy(&bs, original) + if err != nil { + return nil, err + } + debugInfo := lrt.formatJSON(bs.Bytes()) + if debugInfo != "" { + log.Printf("[DEBUG] OpenStack Response Body: %s", debugInfo) + } + return ioutil.NopCloser(strings.NewReader(bs.String())), nil + } + + log.Printf("[DEBUG] Not logging because OpenStack response body isn't JSON") + return original, nil +} + +// formatJSON will try to pretty-format a JSON body. +// It will also mask known fields which contain sensitive information. +func (lrt *LogRoundTripper) formatJSON(raw []byte) string { + var data map[string]interface{} + + err := json.Unmarshal(raw, &data) + if err != nil { + log.Printf("[DEBUG] Unable to parse OpenStack JSON: %s", err) + return string(raw) + } + + // Mask known password fields + if v, ok := data["auth"].(map[string]interface{}); ok { + if v, ok := v["identity"].(map[string]interface{}); ok { + if v, ok := v["password"].(map[string]interface{}); ok { + if v, ok := v["user"].(map[string]interface{}); ok { + v["password"] = "***" + } + } + } + } + + // Ignore the catalog + if v, ok := data["token"].(map[string]interface{}); ok { + if _, ok := v["catalog"]; ok { + return "" + } + } + + pretty, err := json.MarshalIndent(data, "", " ") + if err != nil { + log.Printf("[DEBUG] Unable to re-marshal OpenStack JSON: %s", err) + return string(raw) + } + + return string(pretty) +} + +// redactHeaders processes a headers object, returning a redacted list +func redactHeaders(headers http.Header) (processedHeaders []string) { + for name, header := range headers { + var sensitive bool + + for _, redact_header := range REDACT_HEADERS { + if strings.ToLower(name) == strings.ToLower(redact_header) { + sensitive = true + } + } + + for _, v := range header { + if sensitive { + processedHeaders = append(processedHeaders, fmt.Sprintf("%v: %v", name, "***")) + } else { + processedHeaders = append(processedHeaders, fmt.Sprintf("%v: %v", name, v)) + } + } + } + return +} + +// formatHeaders processes a headers object plus a deliminator, returning a string +func formatHeaders(headers http.Header) string { + redactedHeaders := redactHeaders(headers) + sort.Strings(redactedHeaders) + + return strings.Join(redactedHeaders, "\n") +} diff --git a/acceptance/openstack/compute/v2/attachinterfaces_test.go b/acceptance/openstack/compute/v2/attachinterfaces_test.go new file mode 100644 index 000000000..0f8c82d2b --- /dev/null +++ b/acceptance/openstack/compute/v2/attachinterfaces_test.go @@ -0,0 +1,51 @@ +// +build acceptance compute servers + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestAttachDetachInterface(t *testing.T) { + clients.RequireLong(t) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + iface, err := AttachInterface(t, client, server.ID) + th.AssertNoErr(t, err) + defer DetachInterface(t, client, server.ID, iface.PortID) + + tools.PrintResource(t, iface) + + server, err = servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + + var found bool + for _, networkAddresses := range server.Addresses[choices.NetworkName].([]interface{}) { + address := networkAddresses.(map[string]interface{}) + if address["OS-EXT-IPS:type"] == "fixed" { + fixedIP := address["addr"].(string) + + for _, v := range iface.FixedIPs { + if fixedIP == v.IPAddress { + found = true + } + } + } + } + + th.AssertEquals(t, found, true) +} diff --git a/acceptance/openstack/compute/v2/availabilityzones_test.go b/acceptance/openstack/compute/v2/availabilityzones_test.go new file mode 100644 index 000000000..76be02dde --- /dev/null +++ b/acceptance/openstack/compute/v2/availabilityzones_test.go @@ -0,0 +1,60 @@ +// +build acceptance compute availabilityzones + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/availabilityzones" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestAvailabilityZonesList(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + allPages, err := availabilityzones.List(client).AllPages() + th.AssertNoErr(t, err) + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + th.AssertNoErr(t, err) + + var found bool + for _, zoneInfo := range availabilityZoneInfo { + tools.PrintResource(t, zoneInfo) + + if zoneInfo.ZoneName == "nova" { + found = true + } + } + + th.AssertEquals(t, found, true) +} + +/* +func TestAvailabilityZonesListDetail(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + allPages, err := availabilityzones.ListDetail(client).AllPages() + th.AssertNoErr(t, err) + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + th.AssertNoErr(t, err) + + var found bool + for _, zoneInfo := range availabilityZoneInfo { + tools.PrintResource(t, zoneInfo) + + if zoneInfo.ZoneName == "nova" { + found = true + } + } + + th.AssertEquals(t, found, true) +} +*/ diff --git a/acceptance/openstack/compute/v2/bootfromvolume_test.go b/acceptance/openstack/compute/v2/bootfromvolume_test.go new file mode 100644 index 000000000..9eb057620 --- /dev/null +++ b/acceptance/openstack/compute/v2/bootfromvolume_test.go @@ -0,0 +1,268 @@ +// +build acceptance compute bootfromvolume + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + blockstorage "github.com/huaweicloud/golangsdk/acceptance/openstack/blockstorage/v2" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/bootfromvolume" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/volumeattach" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestBootFromImage(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + blockDevices := []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: choices.ImageID, + }, + } + + server, err := CreateBootableVolumeServer(t, client, blockDevices) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + tools.PrintResource(t, server) + + th.AssertEquals(t, server.Image["id"], choices.ImageID) +} + +func TestBootFromNewVolume(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + blockDevices := []bootfromvolume.BlockDevice{ + { + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceImage, + UUID: choices.ImageID, + VolumeSize: 2, + }, + } + + server, err := CreateBootableVolumeServer(t, client, blockDevices) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + attachPages, err := volumeattach.List(client, server.ID).AllPages() + th.AssertNoErr(t, err) + + attachments, err := volumeattach.ExtractVolumeAttachments(attachPages) + th.AssertNoErr(t, err) + + tools.PrintResource(t, server) + tools.PrintResource(t, attachments) + + if server.Image != nil { + t.Fatalf("server image should be nil") + } + + th.AssertEquals(t, len(attachments), 1) + + // TODO: volumes_attached extension +} + +func TestBootFromExistingVolume(t *testing.T) { + clients.RequireLong(t) + + computeClient, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + blockStorageClient, err := clients.NewBlockStorageV2Client() + th.AssertNoErr(t, err) + + volume, err := blockstorage.CreateVolumeFromImage(t, blockStorageClient) + th.AssertNoErr(t, err) + + tools.PrintResource(t, volume) + + blockDevices := []bootfromvolume.BlockDevice{ + { + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceVolume, + UUID: volume.ID, + }, + } + + server, err := CreateBootableVolumeServer(t, computeClient, blockDevices) + th.AssertNoErr(t, err) + defer DeleteServer(t, computeClient, server) + + attachPages, err := volumeattach.List(computeClient, server.ID).AllPages() + th.AssertNoErr(t, err) + + attachments, err := volumeattach.ExtractVolumeAttachments(attachPages) + th.AssertNoErr(t, err) + + tools.PrintResource(t, server) + tools.PrintResource(t, attachments) + + if server.Image != nil { + t.Fatalf("server image should be nil") + } + + th.AssertEquals(t, len(attachments), 1) + th.AssertEquals(t, attachments[0].VolumeID, volume.ID) + // TODO: volumes_attached extension +} + +func TestBootFromMultiEphemeralServer(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + blockDevices := []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + SourceType: bootfromvolume.SourceImage, + UUID: choices.ImageID, + VolumeSize: 5, + }, + { + BootIndex: -1, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + { + BootIndex: -1, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + } + + server, err := CreateMultiEphemeralServer(t, client, blockDevices) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + tools.PrintResource(t, server) +} + +func TestAttachNewVolume(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + blockDevices := []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: choices.ImageID, + }, + { + BootIndex: 1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 2, + }, + } + + server, err := CreateBootableVolumeServer(t, client, blockDevices) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + attachPages, err := volumeattach.List(client, server.ID).AllPages() + th.AssertNoErr(t, err) + + attachments, err := volumeattach.ExtractVolumeAttachments(attachPages) + th.AssertNoErr(t, err) + + tools.PrintResource(t, server) + tools.PrintResource(t, attachments) + + th.AssertEquals(t, server.Image["id"], choices.ImageID) + th.AssertEquals(t, len(attachments), 1) + + // TODO: volumes_attached extension +} + +func TestAttachExistingVolume(t *testing.T) { + clients.RequireLong(t) + + computeClient, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + blockStorageClient, err := clients.NewBlockStorageV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + volume, err := blockstorage.CreateVolume(t, blockStorageClient) + th.AssertNoErr(t, err) + + blockDevices := []bootfromvolume.BlockDevice{ + { + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: choices.ImageID, + }, + { + BootIndex: 1, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceVolume, + UUID: volume.ID, + }, + } + + server, err := CreateBootableVolumeServer(t, computeClient, blockDevices) + th.AssertNoErr(t, err) + defer DeleteServer(t, computeClient, server) + + attachPages, err := volumeattach.List(computeClient, server.ID).AllPages() + th.AssertNoErr(t, err) + + attachments, err := volumeattach.ExtractVolumeAttachments(attachPages) + th.AssertNoErr(t, err) + + tools.PrintResource(t, server) + tools.PrintResource(t, attachments) + + th.AssertEquals(t, server.Image["id"], choices.ImageID) + th.AssertEquals(t, len(attachments), 1) + th.AssertEquals(t, attachments[0].VolumeID, volume.ID) + + // TODO: volumes_attached extension +} diff --git a/acceptance/openstack/compute/v2/compute.go b/acceptance/openstack/compute/v2/compute.go new file mode 100644 index 000000000..cc76dfcc4 --- /dev/null +++ b/acceptance/openstack/compute/v2/compute.go @@ -0,0 +1,1007 @@ +// Package v2 contains common functions for creating compute-based resources +// for use in acceptance tests. See the `*_test.go` files for example usages. +package v2 + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" + "testing" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + //"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/attachinterfaces" + //"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + //dsr "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/floatingips" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/keypairs" + //"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks" + //"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" + //"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/secgroups" + //"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/tenantnetworks" + //"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + //"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + th "github.com/huaweicloud/golangsdk/testhelper" + + //"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/aggregates" + "golang.org/x/crypto/ssh" +) + +// AssociateFloatingIP will associate a floating IP with an instance. An error +// will be returned if the floating IP was unable to be associated. +func AssociateFloatingIP(t *testing.T, client *golangsdk.ServiceClient, floatingIP *floatingips.FloatingIP, server *servers.Server) error { + associateOpts := floatingips.AssociateOpts{ + FloatingIP: floatingIP.IP, + } + + t.Logf("Attempting to associate floating IP %s to instance %s", floatingIP.IP, server.ID) + err := floatingips.AssociateInstance(client, server.ID, associateOpts).ExtractErr() + if err != nil { + return err + } + + return nil +} + +// AssociateFloatingIPWithFixedIP will associate a floating IP with an +// instance's specific fixed IP. An error will be returend if the floating IP +// was unable to be associated. +func AssociateFloatingIPWithFixedIP(t *testing.T, client *golangsdk.ServiceClient, floatingIP *floatingips.FloatingIP, server *servers.Server, fixedIP string) error { + associateOpts := floatingips.AssociateOpts{ + FloatingIP: floatingIP.IP, + FixedIP: fixedIP, + } + + t.Logf("Attempting to associate floating IP %s to fixed IP %s on instance %s", floatingIP.IP, fixedIP, server.ID) + err := floatingips.AssociateInstance(client, server.ID, associateOpts).ExtractErr() + if err != nil { + return err + } + + return nil +} + +// AttachInterface will create and attach an interface on a given server. +// An error will returned if the interface could not be created. +func AttachInterface(t *testing.T, client *golangsdk.ServiceClient, serverID string) (*attachinterfaces.Interface, error) { + t.Logf("Attempting to attach interface to server %s", serverID) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return nil, err + } + + createOpts := attachinterfaces.CreateOpts{ + NetworkID: networkID, + } + + iface, err := attachinterfaces.Create(client, serverID, createOpts).Extract() + if err != nil { + return nil, err + } + + t.Logf("Successfully created interface %s on server %s", iface.PortID, serverID) + + return iface, nil +} + +// CreateAggregate will create an aggregate with random name and available zone. +// An error will be returned if the aggregate could not be created. +/* +func CreateAggregate(t *testing.T, client *golangsdk.ServiceClient) (*aggregates.Aggregate, error) { + aggregateName := tools.RandomString("aggregate_", 5) + availabilityZone := tools.RandomString("zone_", 5) + t.Logf("Attempting to create aggregate %s", aggregateName) + + createOpts := aggregates.CreateOpts{ + Name: aggregateName, + AvailabilityZone: availabilityZone, + } + + aggregate, err := aggregates.Create(client, createOpts).Extract() + if err != nil { + return nil, err + } + + t.Logf("Successfully created aggregate %d", aggregate.ID) + + aggregate, err = aggregates.Get(client, aggregate.ID).Extract() + if err != nil { + return nil, err + } + + th.AssertEquals(t, aggregate.Name, aggregateName) + th.AssertEquals(t, aggregate.AvailabilityZone, availabilityZone) + + return aggregate, nil +} +*/ + +// CreateBootableVolumeServer works like CreateServer but is configured with +// one or more block devices defined by passing in []bootfromvolume.BlockDevice. +// An error will be returned if a server was unable to be created. +/* +func CreateBootableVolumeServer(t *testing.T, client *golangsdk.ServiceClient, blockDevices []bootfromvolume.BlockDevice) (*servers.Server, error) { + var server *servers.Server + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return server, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create bootable volume server: %s", name) + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + Networks: []servers.Network{ + servers.Network{UUID: networkID}, + }, + } + + if blockDevices[0].SourceType == bootfromvolume.SourceImage && blockDevices[0].DestinationType == bootfromvolume.DestinationLocal { + serverCreateOpts.ImageRef = blockDevices[0].UUID + } + + server, err = bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + }).Extract() + + if err != nil { + return server, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return server, err + } + + newServer, err := servers.Get(client, server.ID).Extract() + if err != nil { + return nil, err + } + + th.AssertEquals(t, newServer.Name, name) + th.AssertEquals(t, newServer.Flavor["id"], choices.FlavorID) + + return newServer, nil +} +*/ + +// CreateDefaultRule will create a default security group rule with a +// random port range between 80 and 90. An error will be returned if +// a default rule was unable to be created. +/* +func CreateDefaultRule(t *testing.T, client *golangsdk.ServiceClient) (dsr.DefaultRule, error) { + createOpts := dsr.CreateOpts{ + FromPort: tools.RandomInt(80, 89), + ToPort: tools.RandomInt(90, 99), + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + defaultRule, err := dsr.Create(client, createOpts).Extract() + if err != nil { + return *defaultRule, err + } + + t.Logf("Created default rule: %s", defaultRule.ID) + + return *defaultRule, nil +} +*/ + +// CreateFlavor will create a flavor with a random name. +// An error will be returned if the flavor could not be created. +/* +func CreateFlavor(t *testing.T, client *golangsdk.ServiceClient) (*flavors.Flavor, error) { + flavorName := tools.RandomString("flavor_", 5) + t.Logf("Attempting to create flavor %s", flavorName) + + isPublic := true + createOpts := flavors.CreateOpts{ + Name: flavorName, + RAM: 1, + VCPUs: 1, + Disk: golangsdk.IntToPointer(1), + IsPublic: &isPublic, + } + + flavor, err := flavors.Create(client, createOpts).Extract() + if err != nil { + return nil, err + } + + t.Logf("Successfully created flavor %s", flavor.ID) + + th.AssertEquals(t, flavor.Name, flavorName) + th.AssertEquals(t, flavor.RAM, 1) + th.AssertEquals(t, flavor.Disk, 1) + th.AssertEquals(t, flavor.VCPUs, 1) + th.AssertEquals(t, flavor.IsPublic, true) + + return flavor, nil +} +*/ + +// CreateFloatingIP will allocate a floating IP. +// An error will be returend if one was unable to be allocated. +func CreateFloatingIP(t *testing.T, client *golangsdk.ServiceClient) (*floatingips.FloatingIP, error) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + createOpts := floatingips.CreateOpts{ + Pool: choices.FloatingIPPoolName, + } + floatingIP, err := floatingips.Create(client, createOpts).Extract() + if err != nil { + return floatingIP, err + } + + t.Logf("Created floating IP: %s", floatingIP.ID) + return floatingIP, nil +} + +func createKey() (string, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", err + } + + publicKey := privateKey.PublicKey + pub, err := ssh.NewPublicKey(&publicKey) + if err != nil { + return "", err + } + + pubBytes := ssh.MarshalAuthorizedKey(pub) + pk := string(pubBytes) + return pk, nil +} + +// CreateKeyPair will create a KeyPair with a random name. An error will occur +// if the keypair failed to be created. An error will be returned if the +// keypair was unable to be created. +func CreateKeyPair(t *testing.T, client *golangsdk.ServiceClient) (*keypairs.KeyPair, error) { + keyPairName := tools.RandomString("keypair_", 5) + + t.Logf("Attempting to create keypair: %s", keyPairName) + createOpts := keypairs.CreateOpts{ + Name: keyPairName, + } + keyPair, err := keypairs.Create(client, createOpts).Extract() + if err != nil { + return keyPair, err + } + + t.Logf("Created keypair: %s", keyPairName) + + th.AssertEquals(t, keyPair.Name, keyPairName) + + return keyPair, nil +} + +// CreateMultiEphemeralServer works like CreateServer but is configured with +// one or more block devices defined by passing in []bootfromvolume.BlockDevice. +// These block devices act like block devices when booting from a volume but +// are actually local ephemeral disks. +// An error will be returned if a server was unable to be created. +/* +func CreateMultiEphemeralServer(t *testing.T, client *golangsdk.ServiceClient, blockDevices []bootfromvolume.BlockDevice) (*servers.Server, error) { + var server *servers.Server + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return server, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create bootable volume server: %s", name) + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + Networks: []servers.Network{ + servers.Network{UUID: networkID}, + }, + } + + server, err = bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + }).Extract() + + if err != nil { + return server, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return server, err + } + + newServer, err := servers.Get(client, server.ID).Extract() + + th.AssertEquals(t, newServer.Name, name) + th.AssertEquals(t, newServer.Flavor["id"], choices.FlavorID) + th.AssertEquals(t, newServer.Image["id"], choices.ImageID) + + return newServer, nil +} +*/ + +// CreatePrivateFlavor will create a private flavor with a random name. +// An error will be returned if the flavor could not be created. +/* +func CreatePrivateFlavor(t *testing.T, client *golangsdk.ServiceClient) (*flavors.Flavor, error) { + flavorName := tools.RandomString("flavor_", 5) + t.Logf("Attempting to create flavor %s", flavorName) + + isPublic := false + createOpts := flavors.CreateOpts{ + Name: flavorName, + RAM: 1, + VCPUs: 1, + Disk: golangsdk.IntToPointer(1), + IsPublic: &isPublic, + } + + flavor, err := flavors.Create(client, createOpts).Extract() + if err != nil { + return nil, err + } + + t.Logf("Successfully created flavor %s", flavor.ID) + + th.AssertEquals(t, flavor.Name, flavorName) + th.AssertEquals(t, flavor.RAM, 1) + th.AssertEquals(t, flavor.Disk, 1) + th.AssertEquals(t, flavor.VCPUs, 1) + th.AssertEquals(t, flavor.IsPublic, false) + + return flavor, nil +} +*/ + +// CreateSecurityGroup will create a security group with a random name. +// An error will be returned if one was failed to be created. +func CreateSecurityGroup(t *testing.T, client *golangsdk.ServiceClient) (*secgroups.SecurityGroup, error) { + name := tools.RandomString("secgroup_", 5) + + createOpts := secgroups.CreateOpts{ + Name: name, + Description: "something", + } + + securityGroup, err := secgroups.Create(client, createOpts).Extract() + if err != nil { + return nil, err + } + + t.Logf("Created security group: %s", securityGroup.ID) + + th.AssertEquals(t, securityGroup.Name, name) + + return securityGroup, nil +} + +// CreateSecurityGroupRule will create a security group rule with a random name +// and a random TCP port range between port 80 and 99. An error will be +// returned if the rule failed to be created. +func CreateSecurityGroupRule(t *testing.T, client *golangsdk.ServiceClient, securityGroupID string) (*secgroups.Rule, error) { + fromPort := tools.RandomInt(80, 89) + toPort := tools.RandomInt(90, 99) + createOpts := secgroups.CreateRuleOpts{ + ParentGroupID: securityGroupID, + FromPort: fromPort, + ToPort: toPort, + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + rule, err := secgroups.CreateRule(client, createOpts).Extract() + if err != nil { + return nil, err + } + + t.Logf("Created security group rule: %s", rule.ID) + + th.AssertEquals(t, rule.FromPort, fromPort) + th.AssertEquals(t, rule.ToPort, toPort) + th.AssertEquals(t, rule.ParentGroupID, securityGroupID) + + return rule, nil +} + +// CreateServer creates a basic instance with a randomly generated name. +// The flavor of the instance will be the value of the OS_FLAVOR_ID environment variable. +// The image will be the value of the OS_IMAGE_ID environment variable. +// The instance will be launched on the network specified in OS_NETWORK_NAME. +// An error will be returned if the instance was unable to be created. +func CreateServer(t *testing.T, client *golangsdk.ServiceClient) (*servers.Server, error) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return nil, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s", name) + + pwd := tools.MakeNewPassword("") + + server, err := servers.Create(client, servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + AdminPass: pwd, + Networks: []servers.Network{ + {UUID: networkID}, + }, + Metadata: map[string]string{ + "abc": "def", + }, + Personality: servers.Personality{ + &servers.File{ + Path: "/etc/test", + Contents: []byte("hello world"), + }, + }, + }).Extract() + if err != nil { + return server, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return nil, err + } + + newServer, err := servers.Get(client, server.ID).Extract() + if err != nil { + return nil, err + } + + th.AssertEquals(t, newServer.Name, name) + th.AssertEquals(t, newServer.Flavor["id"], choices.FlavorID) + th.AssertEquals(t, newServer.Image["id"], choices.ImageID) + + return newServer, nil +} + +// CreateServerWithoutImageRef creates a basic instance with a randomly generated name. +// The flavor of the instance will be the value of the OS_FLAVOR_ID environment variable. +// The image is intentionally missing to trigger an error. +// The instance will be launched on the network specified in OS_NETWORK_NAME. +// An error will be returned if the instance was unable to be created. +func CreateServerWithoutImageRef(t *testing.T, client *golangsdk.ServiceClient) (*servers.Server, error) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return nil, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s", name) + + pwd := tools.MakeNewPassword("") + + server, err := servers.Create(client, servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + AdminPass: pwd, + Networks: []servers.Network{ + {UUID: networkID}, + }, + Personality: servers.Personality{ + &servers.File{ + Path: "/etc/test", + Contents: []byte("hello world"), + }, + }, + }).Extract() + if err != nil { + return nil, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return nil, err + } + + return server, nil +} + +// CreateServerGroup will create a server with a random name. An error will be +// returned if the server group failed to be created. +/* +func CreateServerGroup(t *testing.T, client *golangsdk.ServiceClient, policy string) (*servergroups.ServerGroup, error) { + name := tools.RandomString("ACPTTEST", 16) + + t.Logf("Attempting to create server group %s", name) + + sg, err := servergroups.Create(client, &servergroups.CreateOpts{ + Name: name, + Policies: []string{policy}, + }).Extract() + + if err != nil { + return nil, err + } + + t.Logf("Successfully created server group %s", name) + + th.AssertEquals(t, sg.Name, name) + + return sg, nil +} +*/ + +// CreateServerInServerGroup works like CreateServer but places the instance in +// a specified Server Group. +/* +func CreateServerInServerGroup(t *testing.T, client *golangsdk.ServiceClient, serverGroup *servergroups.ServerGroup) (*servers.Server, error) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return nil, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s", name) + + pwd := tools.MakeNewPassword("") + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + AdminPass: pwd, + Networks: []servers.Network{ + servers.Network{UUID: networkID}, + }, + } + + schedulerHintsOpts := schedulerhints.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + SchedulerHints: schedulerhints.SchedulerHints{ + Group: serverGroup.ID, + }, + } + server, err := servers.Create(client, schedulerHintsOpts).Extract() + if err != nil { + return nil, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return nil, err + } + + newServer, err := servers.Get(client, server.ID).Extract() + if err != nil { + return nil, err + } + + th.AssertEquals(t, newServer.Name, name) + th.AssertEquals(t, newServer.Flavor["id"], choices.FlavorID) + th.AssertEquals(t, newServer.Image["id"], choices.ImageID) + + return newServer, nil +} +*/ + +// CreateServerWithPublicKey works the same as CreateServer, but additionally +// configures the server with a specified Key Pair name. +func CreateServerWithPublicKey(t *testing.T, client *golangsdk.ServiceClient, keyPairName string) (*servers.Server, error) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + if err != nil { + return nil, err + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s", name) + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + Networks: []servers.Network{ + {UUID: networkID}, + }, + } + + server, err := servers.Create(client, keypairs.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + KeyName: keyPairName, + }).Extract() + if err != nil { + return nil, err + } + + if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + return nil, err + } + + newServer, err := servers.Get(client, server.ID).Extract() + if err != nil { + return nil, err + } + + th.AssertEquals(t, newServer.Name, name) + th.AssertEquals(t, newServer.Flavor["id"], choices.FlavorID) + th.AssertEquals(t, newServer.Image["id"], choices.ImageID) + + return newServer, nil +} + +// CreateVolumeAttachment will attach a volume to a server. An error will be +// returned if the volume failed to attach. +/* +func CreateVolumeAttachment(t *testing.T, client *golangsdk.ServiceClient, blockClient *golangsdk.ServiceClient, server *servers.Server, volume *volumes.Volume) (*volumeattach.VolumeAttachment, error) { + volumeAttachOptions := volumeattach.CreateOpts{ + VolumeID: volume.ID, + } + + t.Logf("Attempting to attach volume %s to server %s", volume.ID, server.ID) + volumeAttachment, err := volumeattach.Create(client, server.ID, volumeAttachOptions).Extract() + if err != nil { + return volumeAttachment, err + } + + if err := volumes.WaitForStatus(blockClient, volume.ID, "in-use", 60); err != nil { + return volumeAttachment, err + } + + return volumeAttachment, nil +} +*/ + +// DeleteAggregate will delete a given host aggregate. A fatal error will occur if +// the aggregate deleting is failed. This works best when using it as a +// deferred function. +/* +func DeleteAggregate(t *testing.T, client *golangsdk.ServiceClient, aggregate *aggregates.Aggregate) { + err := aggregates.Delete(client, aggregate.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete aggregate %d", aggregate.ID) + } + + t.Logf("Deleted aggregate: %d", aggregate.ID) +} +*/ + +// DeleteDefaultRule deletes a default security group rule. +// A fatal error will occur if the rule failed to delete. This works best when +// using it as a deferred function. +/* +func DeleteDefaultRule(t *testing.T, client *golangsdk.ServiceClient, defaultRule dsr.DefaultRule) { + err := dsr.Delete(client, defaultRule.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete default rule %s: %v", defaultRule.ID, err) + } + + t.Logf("Deleted default rule: %s", defaultRule.ID) +} +*/ + +// DeleteFlavor will delete a flavor. A fatal error will occur if the flavor +// could not be deleted. This works best when using it as a deferred function. +/* +func DeleteFlavor(t *testing.T, client *golangsdk.ServiceClient, flavor *flavors.Flavor) { + err := flavors.Delete(client, flavor.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete flavor %s", flavor.ID) + } + + t.Logf("Deleted flavor: %s", flavor.ID) +} +*/ + +// DeleteFloatingIP will de-allocate a floating IP. A fatal error will occur if +// the floating IP failed to de-allocate. This works best when using it as a +// deferred function. +func DeleteFloatingIP(t *testing.T, client *golangsdk.ServiceClient, floatingIP *floatingips.FloatingIP) { + err := floatingips.Delete(client, floatingIP.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete floating IP %s: %v", floatingIP.ID, err) + } + + t.Logf("Deleted floating IP: %s", floatingIP.ID) +} + +// DeleteKeyPair will delete a specified keypair. A fatal error will occur if +// the keypair failed to be deleted. This works best when used as a deferred +// function. +func DeleteKeyPair(t *testing.T, client *golangsdk.ServiceClient, keyPair *keypairs.KeyPair) { + err := keypairs.Delete(client, keyPair.Name).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete keypair %s: %v", keyPair.Name, err) + } + + t.Logf("Deleted keypair: %s", keyPair.Name) +} + +// DeleteSecurityGroup will delete a security group. A fatal error will occur +// if the group failed to be deleted. This works best as a deferred function. +func DeleteSecurityGroup(t *testing.T, client *golangsdk.ServiceClient, securityGroupID string) { + err := secgroups.Delete(client, securityGroupID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete security group %s: %s", securityGroupID, err) + } + + t.Logf("Deleted security group: %s", securityGroupID) +} + +// DeleteSecurityGroupRule will delete a security group rule. A fatal error +// will occur if the rule failed to be deleted. This works best when used +// as a deferred function. +func DeleteSecurityGroupRule(t *testing.T, client *golangsdk.ServiceClient, ruleID string) { + err := secgroups.DeleteRule(client, ruleID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete rule: %v", err) + } + + t.Logf("Deleted security group rule: %s", ruleID) +} + +// DeleteServer deletes an instance via its UUID. +// A fatal error will occur if the instance failed to be destroyed. This works +// best when using it as a deferred function. +func DeleteServer(t *testing.T, client *golangsdk.ServiceClient, server *servers.Server) { + err := servers.Delete(client, server.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete server %s: %s", server.ID, err) + } + + if err := WaitForComputeStatus(client, server, "DELETED"); err != nil { + if _, ok := err.(golangsdk.ErrDefault404); ok { + t.Logf("Deleted server: %s", server.ID) + return + } + t.Fatalf("Error deleting server %s: %s", server.ID, err) + } + + // If we reach this point, the API returned an actual DELETED status + // which is a very short window of time, but happens occasionally. + t.Logf("Deleted server: %s", server.ID) +} + +// DeleteServerGroup will delete a server group. A fatal error will occur if +// the server group failed to be deleted. This works best when used as a +// deferred function. +/* +func DeleteServerGroup(t *testing.T, client *golangsdk.ServiceClient, serverGroup *servergroups.ServerGroup) { + err := servergroups.Delete(client, serverGroup.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete server group %s: %v", serverGroup.ID, err) + } + + t.Logf("Deleted server group %s", serverGroup.ID) +} +*/ + +// DeleteVolumeAttachment will disconnect a volume from an instance. A fatal +// error will occur if the volume failed to detach. This works best when used +// as a deferred function. +/* +func DeleteVolumeAttachment(t *testing.T, client *golangsdk.ServiceClient, blockClient *golangsdk.ServiceClient, server *servers.Server, volumeAttachment *volumeattach.VolumeAttachment) { + + err := volumeattach.Delete(client, server.ID, volumeAttachment.VolumeID).ExtractErr() + if err != nil { + t.Fatalf("Unable to detach volume: %v", err) + } + + if err := volumes.WaitForStatus(blockClient, volumeAttachment.ID, "available", 60); err != nil { + t.Fatalf("Unable to wait for volume: %v", err) + } + t.Logf("Deleted volume: %s", volumeAttachment.VolumeID) +} +*/ + +// DetachInterface will detach an interface from a server. A fatal +// error will occur if the interface could not be detached. This works best +// when used as a deferred function. +func DetachInterface(t *testing.T, client *golangsdk.ServiceClient, serverID, portID string) { + t.Logf("Attempting to detach interface %s from server %s", portID, serverID) + + err := attachinterfaces.Delete(client, serverID, portID).ExtractErr() + if err != nil { + t.Fatalf("Unable to detach interface %s from server %s", portID, serverID) + } + + t.Logf("Detached interface %s from server %s", portID, serverID) +} + +// DisassociateFloatingIP will disassociate a floating IP from an instance. A +// fatal error will occur if the floating IP failed to disassociate. This works +// best when using it as a deferred function. +func DisassociateFloatingIP(t *testing.T, client *golangsdk.ServiceClient, floatingIP *floatingips.FloatingIP, server *servers.Server) { + disassociateOpts := floatingips.DisassociateOpts{ + FloatingIP: floatingIP.IP, + } + + err := floatingips.DisassociateInstance(client, server.ID, disassociateOpts).ExtractErr() + if err != nil { + t.Fatalf("Unable to disassociate floating IP %s from server %s: %v", floatingIP.IP, server.ID, err) + } + + t.Logf("Disassociated floating IP %s from server %s", floatingIP.IP, server.ID) +} + +// GetNetworkIDFromNetworks will return the network ID from a specified network +// UUID using the os-networks API extension. An error will be returned if the +// network could not be retrieved. +/* +func GetNetworkIDFromNetworks(t *testing.T, client *golangsdk.ServiceClient, networkName string) (string, error) { + allPages, err := networks.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + networkList, err := networks.ExtractNetworks(allPages) + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + networkID := "" + for _, network := range networkList { + t.Logf("Network: %v", network) + if network.Label == networkName { + networkID = network.ID + } + } + + t.Logf("Found network ID for %s: %s", networkName, networkID) + + return networkID, nil +} +*/ + +// GetNetworkIDFromTenantNetworks will return the network UUID for a given +// network name using the os-tenant-networks API extension. An error will be +// returned if the network could not be retrieved. +func GetNetworkIDFromTenantNetworks(t *testing.T, client *golangsdk.ServiceClient, networkName string) (string, error) { + allPages, err := tenantnetworks.List(client).AllPages() + if err != nil { + return "", err + } + + allTenantNetworks, err := tenantnetworks.ExtractNetworks(allPages) + if err != nil { + return "", err + } + + for _, network := range allTenantNetworks { + if network.Name == networkName { + return network.ID, nil + } + } + + return "", fmt.Errorf("Failed to obtain network ID for network %s", networkName) +} + +// ImportPublicKey will create a KeyPair with a random name and a specified +// public key. An error will be returned if the keypair failed to be created. +func ImportPublicKey(t *testing.T, client *golangsdk.ServiceClient, publicKey string) (*keypairs.KeyPair, error) { + keyPairName := tools.RandomString("keypair_", 5) + + t.Logf("Attempting to create keypair: %s", keyPairName) + createOpts := keypairs.CreateOpts{ + Name: keyPairName, + PublicKey: publicKey, + } + keyPair, err := keypairs.Create(client, createOpts).Extract() + if err != nil { + return keyPair, err + } + + t.Logf("Created keypair: %s", keyPairName) + + th.AssertEquals(t, keyPair.Name, keyPairName) + th.AssertEquals(t, keyPair.PublicKey, publicKey) + + return keyPair, nil +} + +// ResizeServer performs a resize action on an instance. An error will be +// returned if the instance failed to resize. +// The new flavor that the instance will be resized to is specified in OS_FLAVOR_ID_RESIZE. +func ResizeServer(t *testing.T, client *golangsdk.ServiceClient, server *servers.Server) error { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + opts := &servers.ResizeOpts{ + FlavorRef: choices.FlavorIDResize, + } + if res := servers.Resize(client, server.ID, opts); res.Err != nil { + return res.Err + } + + if err := WaitForComputeStatus(client, server, "VERIFY_RESIZE"); err != nil { + return err + } + + return nil +} + +// WaitForComputeStatus will poll an instance's status until it either matches +// the specified status or the status becomes ERROR. +func WaitForComputeStatus(client *golangsdk.ServiceClient, server *servers.Server, status string) error { + return tools.WaitFor(func() (bool, error) { + latest, err := servers.Get(client, server.ID).Extract() + if err != nil { + return false, err + } + + if latest.Status == status { + // Success! + return true, nil + } + + if latest.Status == "ERROR" { + return false, fmt.Errorf("Instance in ERROR state") + } + + return false, nil + }) +} + +//Convenience method to fill an QuotaSet-UpdateOpts-struct from a QuotaSet-struct +/* +func FillUpdateOptsFromQuotaSet(src quotasets.QuotaSet, dest *quotasets.UpdateOpts) { + dest.FixedIPs = &src.FixedIPs + dest.FloatingIPs = &src.FloatingIPs + dest.InjectedFileContentBytes = &src.InjectedFileContentBytes + dest.InjectedFilePathBytes = &src.InjectedFilePathBytes + dest.InjectedFiles = &src.InjectedFiles + dest.KeyPairs = &src.KeyPairs + dest.RAM = &src.RAM + dest.SecurityGroupRules = &src.SecurityGroupRules + dest.SecurityGroups = &src.SecurityGroups + dest.Cores = &src.Cores + dest.Instances = &src.Instances + dest.ServerGroups = &src.ServerGroups + dest.ServerGroupMembers = &src.ServerGroupMembers + dest.MetadataItems = &src.MetadataItems +} +*/ diff --git a/acceptance/openstack/compute/v2/extension_test.go b/acceptance/openstack/compute/v2/extension_test.go new file mode 100644 index 000000000..e2fa0cbc9 --- /dev/null +++ b/acceptance/openstack/compute/v2/extension_test.go @@ -0,0 +1,46 @@ +// +build acceptance compute extensions + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/common/extensions" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestExtensionsList(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + allPages, err := extensions.List(client).AllPages() + th.AssertNoErr(t, err) + + allExtensions, err := extensions.ExtractExtensions(allPages) + th.AssertNoErr(t, err) + + var found bool + for _, extension := range allExtensions { + tools.PrintResource(t, extension) + + if extension.Name == "SchedulerHints" { + found = true + } + } + + th.AssertEquals(t, found, true) +} + +func TestExtensionsGet(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + extension, err := extensions.Get(client, "os-admin-actions").Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, extension) + + th.AssertEquals(t, extension.Name, "AdminActions") +} diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go new file mode 100644 index 000000000..7a2ffba92 --- /dev/null +++ b/acceptance/openstack/compute/v2/flavors_test.go @@ -0,0 +1,209 @@ +// +build acceptance compute flavors + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/flavors" + th "github.com/huaweicloud/golangsdk/testhelper" + //identity "github.com/huaweicloud/golangsdk/acceptance/openstack/identity/v3" +) + +func TestFlavorsList(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + allPages, err := flavors.Detail(client, nil).AllPages() + th.AssertNoErr(t, err) + + allFlavors, err := flavors.ExtractFlavors(allPages) + th.AssertNoErr(t, err) + + var found bool + for _, flavor := range allFlavors { + tools.PrintResource(t, flavor) + + if flavor.ID == choices.FlavorID { + found = true + } + } + + th.AssertEquals(t, found, true) +} + +func TestFlavorsAccessTypeList(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + flavorAccessTypes := map[string]flavors.AccessType{ + "public": flavors.PublicAccess, + "private": flavors.PrivateAccess, + "all": flavors.AllAccess, + } + + for flavorTypeName, flavorAccessType := range flavorAccessTypes { + t.Logf("** %s flavors: **", flavorTypeName) + allPages, err := flavors.Detail(client, flavors.ListDetailOpts{AccessType: flavorAccessType}).AllPages() + th.AssertNoErr(t, err) + + allFlavors, err := flavors.ExtractFlavors(allPages) + th.AssertNoErr(t, err) + + for _, flavor := range allFlavors { + tools.PrintResource(t, flavor) + } + } +} + +func TestFlavorsGet(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + flavor, err := flavors.Get(client, choices.FlavorID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, flavor) + + th.AssertEquals(t, flavor.ID, choices.FlavorID) +} + +/* +func TestFlavorsCreateDelete(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + flavor, err := CreateFlavor(t, client) + th.AssertNoErr(t, err) + defer DeleteFlavor(t, client, flavor) + + tools.PrintResource(t, flavor) +} +*/ + +/* +func TestFlavorsAccessesList(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + flavor, err := CreatePrivateFlavor(t, client) + th.AssertNoErr(t, err) + defer DeleteFlavor(t, client, flavor) + + allPages, err := flavors.ListAccesses(client, flavor.ID).AllPages() + th.AssertNoErr(t, err) + + allAccesses, err := flavors.ExtractAccesses(allPages) + th.AssertNoErr(t, err) + + th.AssertEquals(t, len(allAccesses), 0) +} +*/ + +/* +func TestFlavorsAccessCRUD(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + identityClient, err := clients.NewIdentityV3Client() + th.AssertNoErr(t, err) + + project, err := identity.CreateProject(t, identityClient, nil) + th.AssertNoErr(t, err) + defer identity.DeleteProject(t, identityClient, project.ID) + + flavor, err := CreatePrivateFlavor(t, client) + th.AssertNoErr(t, err) + defer DeleteFlavor(t, client, flavor) + + addAccessOpts := flavors.AddAccessOpts{ + Tenant: project.ID, + } + + accessList, err := flavors.AddAccess(client, flavor.ID, addAccessOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, len(accessList), 1) + th.AssertEquals(t, accessList[0].TenantID, project.ID) + th.AssertEquals(t, accessList[0].FlavorID, flavor.ID) + + for _, access := range accessList { + tools.PrintResource(t, access) + } + + removeAccessOpts := flavors.RemoveAccessOpts{ + Tenant: project.ID, + } + + accessList, err = flavors.RemoveAccess(client, flavor.ID, removeAccessOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, len(accessList), 0) +} +*/ + +/* +func TestFlavorsExtraSpecsCRUD(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + flavor, err := CreatePrivateFlavor(t, client) + th.AssertNoErr(t, err) + defer DeleteFlavor(t, client, flavor) + + createOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_policy": "CPU-POLICY", + "hw:cpu_thread_policy": "CPU-THREAD-POLICY", + } + createdExtraSpecs, err := flavors.CreateExtraSpecs(client, flavor.ID, createOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, createdExtraSpecs) + + th.AssertEquals(t, len(createdExtraSpecs), 2) + th.AssertEquals(t, createdExtraSpecs["hw:cpu_policy"], "CPU-POLICY") + th.AssertEquals(t, createdExtraSpecs["hw:cpu_thread_policy"], "CPU-THREAD-POLICY") + + err = flavors.DeleteExtraSpec(client, flavor.ID, "hw:cpu_policy").ExtractErr() + th.AssertNoErr(t, err) + + updateOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_thread_policy": "CPU-THREAD-POLICY-BETTER", + } + updatedExtraSpec, err := flavors.UpdateExtraSpec(client, flavor.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, updatedExtraSpec) + + allExtraSpecs, err := flavors.ListExtraSpecs(client, flavor.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, allExtraSpecs) + + th.AssertEquals(t, len(allExtraSpecs), 1) + th.AssertEquals(t, allExtraSpecs["hw:cpu_thread_policy"], "CPU-THREAD-POLICY-BETTER") + + spec, err := flavors.GetExtraSpec(client, flavor.ID, "hw:cpu_thread_policy").Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, spec) + + th.AssertEquals(t, spec["hw:cpu_thread_policy"], "CPU-THREAD-POLICY-BETTER") +} +*/ diff --git a/acceptance/openstack/compute/v2/floatingip_test.go b/acceptance/openstack/compute/v2/floatingip_test.go new file mode 100644 index 000000000..dbc8e700c --- /dev/null +++ b/acceptance/openstack/compute/v2/floatingip_test.go @@ -0,0 +1,123 @@ +// +build acceptance compute servers + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/floatingips" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestFloatingIPsCreateDelete(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + floatingIP, err := CreateFloatingIP(t, client) + th.AssertNoErr(t, err) + defer DeleteFloatingIP(t, client, floatingIP) + + tools.PrintResource(t, floatingIP) + + allPages, err := floatingips.List(client).AllPages() + th.AssertNoErr(t, err) + + allFloatingIPs, err := floatingips.ExtractFloatingIPs(allPages) + th.AssertNoErr(t, err) + + var found bool + for _, fip := range allFloatingIPs { + tools.PrintResource(t, floatingIP) + + if fip.ID == floatingIP.ID { + found = true + } + } + + th.AssertEquals(t, found, true) + + fip, err := floatingips.Get(client, floatingIP.ID).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, floatingIP.ID, fip.ID) +} + +func TestFloatingIPsAssociate(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + floatingIP, err := CreateFloatingIP(t, client) + th.AssertNoErr(t, err) + defer DeleteFloatingIP(t, client, floatingIP) + + tools.PrintResource(t, floatingIP) + + err = AssociateFloatingIP(t, client, floatingIP, server) + th.AssertNoErr(t, err) + defer DisassociateFloatingIP(t, client, floatingIP, server) + + newFloatingIP, err := floatingips.Get(client, floatingIP.ID).Extract() + th.AssertNoErr(t, err) + + t.Logf("Floating IP %s is associated with Fixed IP %s", floatingIP.IP, newFloatingIP.FixedIP) + + tools.PrintResource(t, newFloatingIP) + + th.AssertEquals(t, newFloatingIP.InstanceID, server.ID) +} + +func TestFloatingIPsFixedIPAssociate(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + newServer, err := servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + + floatingIP, err := CreateFloatingIP(t, client) + th.AssertNoErr(t, err) + defer DeleteFloatingIP(t, client, floatingIP) + + tools.PrintResource(t, floatingIP) + + var fixedIP string + for _, networkAddresses := range newServer.Addresses[choices.NetworkName].([]interface{}) { + address := networkAddresses.(map[string]interface{}) + if address["OS-EXT-IPS:type"] == "fixed" { + if address["version"].(float64) == 4 { + fixedIP = address["addr"].(string) + } + } + } + + err = AssociateFloatingIPWithFixedIP(t, client, floatingIP, newServer, fixedIP) + th.AssertNoErr(t, err) + defer DisassociateFloatingIP(t, client, floatingIP, newServer) + + newFloatingIP, err := floatingips.Get(client, floatingIP.ID).Extract() + th.AssertNoErr(t, err) + + t.Logf("Floating IP %s is associated with Fixed IP %s", floatingIP.IP, newFloatingIP.FixedIP) + + tools.PrintResource(t, newFloatingIP) + + th.AssertEquals(t, newFloatingIP.InstanceID, server.ID) + th.AssertEquals(t, newFloatingIP.FixedIP, fixedIP) +} diff --git a/acceptance/openstack/compute/v2/images_test.go b/acceptance/openstack/compute/v2/images_test.go new file mode 100644 index 000000000..704bcda00 --- /dev/null +++ b/acceptance/openstack/compute/v2/images_test.go @@ -0,0 +1,52 @@ +// +build acceptance compute images + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/images" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestImagesList(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + allPages, err := images.ListDetail(client, nil).AllPages() + th.AssertNoErr(t, err) + + allImages, err := images.ExtractImages(allPages) + th.AssertNoErr(t, err) + + var found bool + for _, image := range allImages { + tools.PrintResource(t, image) + + if image.ID == choices.ImageID { + found = true + } + } + + th.AssertEquals(t, found, true) +} + +func TestImagesGet(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + image, err := images.Get(client, choices.ImageID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, image) + + th.AssertEquals(t, choices.ImageID, image.ID) +} diff --git a/acceptance/openstack/compute/v2/keypairs_test.go b/acceptance/openstack/compute/v2/keypairs_test.go new file mode 100644 index 000000000..1696429ff --- /dev/null +++ b/acceptance/openstack/compute/v2/keypairs_test.go @@ -0,0 +1,79 @@ +// +build acceptance compute keypairs + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/keypairs" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +const keyName = "gophercloud_test_key_pair" + +func TestKeypairsCreateDelete(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + keyPair, err := CreateKeyPair(t, client) + th.AssertNoErr(t, err) + defer DeleteKeyPair(t, client, keyPair) + + tools.PrintResource(t, keyPair) + + allPages, err := keypairs.List(client, nil).AllPages() + th.AssertNoErr(t, err) + + allKeys, err := keypairs.ExtractKeyPairs(allPages) + th.AssertNoErr(t, err) + + var found bool + for _, kp := range allKeys { + tools.PrintResource(t, kp) + + if kp.Name == keyPair.Name { + found = true + } + } + + th.AssertEquals(t, found, true) +} + +func TestKeypairsImportPublicKey(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + publicKey, err := createKey() + th.AssertNoErr(t, err) + + keyPair, err := ImportPublicKey(t, client, publicKey) + th.AssertNoErr(t, err) + defer DeleteKeyPair(t, client, keyPair) + + tools.PrintResource(t, keyPair) +} + +func TestKeypairsServerCreateWithKey(t *testing.T) { + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + publicKey, err := createKey() + th.AssertNoErr(t, err) + + keyPair, err := ImportPublicKey(t, client, publicKey) + th.AssertNoErr(t, err) + defer DeleteKeyPair(t, client, keyPair) + + server, err := CreateServerWithPublicKey(t, client, keyPair.Name) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + server, err = servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, server.KeyName, keyPair.Name) +} diff --git a/acceptance/openstack/compute/v2/pkg.go b/acceptance/openstack/compute/v2/pkg.go new file mode 100644 index 000000000..a57c1e7bf --- /dev/null +++ b/acceptance/openstack/compute/v2/pkg.go @@ -0,0 +1,2 @@ +// Package v2 package contains acceptance tests for the Openstack Compute V2 service. +package v2 diff --git a/acceptance/openstack/compute/v2/secgroup_test.go b/acceptance/openstack/compute/v2/secgroup_test.go new file mode 100644 index 000000000..69760af8f --- /dev/null +++ b/acceptance/openstack/compute/v2/secgroup_test.go @@ -0,0 +1,140 @@ +// +build acceptance compute secgroups + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/secgroups" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestSecGroupsList(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + allPages, err := secgroups.List(client).AllPages() + th.AssertNoErr(t, err) + + allSecGroups, err := secgroups.ExtractSecurityGroups(allPages) + th.AssertNoErr(t, err) + + var found bool + for _, secgroup := range allSecGroups { + tools.PrintResource(t, secgroup) + + if secgroup.Name == "default" { + found = true + } + } + + th.AssertEquals(t, found, true) +} + +func TestSecGroupsCRUD(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + securityGroup, err := CreateSecurityGroup(t, client) + th.AssertNoErr(t, err) + defer DeleteSecurityGroup(t, client, securityGroup.ID) + + tools.PrintResource(t, securityGroup) + + newName := tools.RandomString("secgroup_", 4) + updateOpts := secgroups.UpdateOpts{ + Name: newName, + Description: tools.RandomString("dec_", 10), + } + updatedSecurityGroup, err := secgroups.Update(client, securityGroup.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, updatedSecurityGroup) + + t.Logf("Updated %s's name to %s", updatedSecurityGroup.ID, updatedSecurityGroup.Name) + + th.AssertEquals(t, updatedSecurityGroup.Name, newName) +} + +func TestSecGroupsRuleCreate(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + securityGroup, err := CreateSecurityGroup(t, client) + th.AssertNoErr(t, err) + defer DeleteSecurityGroup(t, client, securityGroup.ID) + + tools.PrintResource(t, securityGroup) + + rule, err := CreateSecurityGroupRule(t, client, securityGroup.ID) + th.AssertNoErr(t, err) + defer DeleteSecurityGroupRule(t, client, rule.ID) + + tools.PrintResource(t, rule) + + newSecurityGroup, err := secgroups.Get(client, securityGroup.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, newSecurityGroup) + + th.AssertEquals(t, len(newSecurityGroup.Rules), 1) +} + +func TestSecGroupsAddGroupToServer(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + securityGroup, err := CreateSecurityGroup(t, client) + th.AssertNoErr(t, err) + defer DeleteSecurityGroup(t, client, securityGroup.ID) + + rule, err := CreateSecurityGroupRule(t, client, securityGroup.ID) + th.AssertNoErr(t, err) + defer DeleteSecurityGroupRule(t, client, rule.ID) + + t.Logf("Adding group %s to server %s", securityGroup.ID, server.ID) + err = secgroups.AddServer(client, server.ID, securityGroup.Name).ExtractErr() + th.AssertNoErr(t, err) + + server, err = servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, server) + + var found bool + for _, sg := range server.SecurityGroups { + if sg["name"] == securityGroup.Name { + found = true + } + } + + th.AssertEquals(t, found, true) + + t.Logf("Removing group %s from server %s", securityGroup.ID, server.ID) + err = secgroups.RemoveServer(client, server.ID, securityGroup.Name).ExtractErr() + th.AssertNoErr(t, err) + + server, err = servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + + found = false + + tools.PrintResource(t, server) + + for _, sg := range server.SecurityGroups { + if sg["name"] == securityGroup.Name { + found = true + } + } + + th.AssertEquals(t, found, false) +} diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go new file mode 100644 index 000000000..46d237e91 --- /dev/null +++ b/acceptance/openstack/compute/v2/servers_test.go @@ -0,0 +1,458 @@ +// +build acceptance compute servers + +package v2 + +import ( + "strings" + "testing" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/attachinterfaces" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/availabilityzones" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/extendedstatus" + //"github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/lockunlock" + //"github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/pauseunpause" + //"github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/suspendresume" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestServersCreateDestroy(t *testing.T) { + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + allPages, err := servers.List(client, servers.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + + allServers, err := servers.ExtractServers(allPages) + th.AssertNoErr(t, err) + + var found bool + for _, s := range allServers { + tools.PrintResource(t, server) + + if s.ID == server.ID { + found = true + } + } + + th.AssertEquals(t, found, true) + + //allAddressPages, err := servers.ListAddresses(client, server.ID).AllPages() + //th.AssertNoErr(t, err) + + //allAddresses, err := servers.ExtractAddresses(allAddressPages) + //th.AssertNoErr(t, err) + + //for network, address := range allAddresses { + // t.Logf("Addresses on %s: %+v", network, address) + //} + + allInterfacePages, err := attachinterfaces.List(client, server.ID).AllPages() + th.AssertNoErr(t, err) + + allInterfaces, err := attachinterfaces.ExtractInterfaces(allInterfacePages) + th.AssertNoErr(t, err) + + for _, iface := range allInterfaces { + t.Logf("Interfaces: %+v", iface) + } + + _ = choices + //allNetworkAddressPages, err := servers.ListAddressesByNetwork(client, server.ID, choices.NetworkName).AllPages() + //th.AssertNoErr(t, err) + + //allNetworkAddresses, err := servers.ExtractNetworkAddresses(allNetworkAddressPages) + //th.AssertNoErr(t, err) + + //t.Logf("Addresses on %s:", choices.NetworkName) + //for _, address := range allNetworkAddresses { + // t.Logf("%+v", address) + //} +} + +func TestServersWithExtensionsCreateDestroy(t *testing.T) { + + var extendedServer struct { + servers.Server + availabilityzones.ServerAvailabilityZoneExt + extendedstatus.ServerExtendedStatusExt + } + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + err = servers.Get(client, server.ID).ExtractInto(&extendedServer) + th.AssertNoErr(t, err) + tools.PrintResource(t, extendedServer) + + th.AssertEquals(t, extendedServer.AvailabilityZone, "nova") + th.AssertEquals(t, int(extendedServer.PowerState), extendedstatus.RUNNING) + th.AssertEquals(t, extendedServer.TaskState, "") + th.AssertEquals(t, extendedServer.VmState, "active") +} + +func TestServersWithoutImageRef(t *testing.T) { + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServerWithoutImageRef(t, client) + if err != nil { + if err400, ok := err.(*golangsdk.ErrUnexpectedResponseCode); ok { + if !strings.Contains("Missing imageRef attribute", string(err400.Body)) { + defer DeleteServer(t, client, server) + } + } + } +} + +/* +func TestServersUpdate(t *testing.T) { + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + alternateName := tools.RandomString("ACPTTEST", 16) + for alternateName == server.Name { + alternateName = tools.RandomString("ACPTTEST", 16) + } + + t.Logf("Attempting to rename the server to %s.", alternateName) + + updateOpts := servers.UpdateOpts{ + Name: alternateName, + } + + updated, err := servers.Update(client, server.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, updated.ID, server.ID) + + err = tools.WaitFor(func() (bool, error) { + latest, err := servers.Get(client, updated.ID).Extract() + if err != nil { + return false, err + } + + return latest.Name == alternateName, nil + }) +} +*/ + +func TestServersMetadata(t *testing.T) { + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + tools.PrintResource(t, server) + + metadata, err := servers.UpdateMetadata(client, server.ID, servers.MetadataOpts{ + "foo": "bar", + "this": "that", + }).Extract() + th.AssertNoErr(t, err) + t.Logf("UpdateMetadata result: %+v\n", metadata) + + server, err = servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, server) + + expectedMetadata := map[string]string{ + "abc": "def", + "foo": "bar", + "this": "that", + } + th.AssertDeepEquals(t, expectedMetadata, server.Metadata) + + err = servers.DeleteMetadatum(client, server.ID, "foo").ExtractErr() + th.AssertNoErr(t, err) + + server, err = servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, server) + + expectedMetadata = map[string]string{ + "abc": "def", + "this": "that", + } + th.AssertDeepEquals(t, expectedMetadata, server.Metadata) + + metadata, err = servers.CreateMetadatum(client, server.ID, servers.MetadatumOpts{ + "foo": "baz", + }).Extract() + th.AssertNoErr(t, err) + t.Logf("CreateMetadatum result: %+v\n", metadata) + + server, err = servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, server) + + expectedMetadata = map[string]string{ + "abc": "def", + "this": "that", + "foo": "baz", + } + th.AssertDeepEquals(t, expectedMetadata, server.Metadata) + + metadata, err = servers.Metadatum(client, server.ID, "foo").Extract() + th.AssertNoErr(t, err) + t.Logf("Metadatum result: %+v\n", metadata) + th.AssertEquals(t, "baz", metadata["foo"]) + + metadata, err = servers.Metadata(client, server.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Metadata result: %+v\n", metadata) + + th.AssertDeepEquals(t, expectedMetadata, metadata) + + metadata, err = servers.ResetMetadata(client, server.ID, servers.MetadataOpts{}).Extract() + th.AssertNoErr(t, err) + t.Logf("ResetMetadata result: %+v\n", metadata) + th.AssertDeepEquals(t, map[string]string{}, metadata) +} + +/* +func TestServersActionChangeAdminPassword(t *testing.T) { + clients.RequireLong(t) + clients.RequireGuestAgent(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + randomPassword := tools.MakeNewPassword(server.AdminPass) + res := servers.ChangeAdminPassword(client, server.ID, randomPassword) + th.AssertNoErr(t, res.Err) + + if err = WaitForComputeStatus(client, server, "PASSWORD"); err != nil { + t.Fatal(err) + } + + if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} +*/ + +func TestServersActionReboot(t *testing.T) { + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + rebootOpts := &servers.RebootOpts{ + Type: servers.SoftReboot, + } + + t.Logf("Attempting reboot of server %s", server.ID) + res := servers.Reboot(client, server.ID, rebootOpts) + th.AssertNoErr(t, res.Err) + + if err = WaitForComputeStatus(client, server, "REBOOT"); err != nil { + t.Fatal(err) + } + + if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestServersActionRebuild(t *testing.T) { + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + t.Logf("Attempting to rebuild server %s", server.ID) + + rebuildOpts := servers.RebuildOpts{ + Name: tools.RandomString("ACPTTEST", 16), + AdminPass: tools.MakeNewPassword(server.AdminPass), + ImageID: choices.ImageID, + } + + rebuilt, err := servers.Rebuild(client, server.ID, rebuildOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, rebuilt.ID, server.ID) + + if err = WaitForComputeStatus(client, rebuilt, "REBUILD"); err != nil { + t.Fatal(err) + } + + if err = WaitForComputeStatus(client, rebuilt, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestServersActionResizeConfirm(t *testing.T) { + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + t.Logf("Attempting to resize server %s", server.ID) + ResizeServer(t, client, server) + + t.Logf("Attempting to confirm resize for server %s", server.ID) + if res := servers.ConfirmResize(client, server.ID); res.Err != nil { + t.Fatal(res.Err) + } + + if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + server, err = servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, server.Flavor["id"], choices.FlavorIDResize) +} + +func TestServersActionResizeRevert(t *testing.T) { + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + t.Logf("Attempting to resize server %s", server.ID) + ResizeServer(t, client, server) + + t.Logf("Attempting to revert resize for server %s", server.ID) + if res := servers.RevertResize(client, server.ID); res.Err != nil { + t.Fatal(res.Err) + } + + if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + server, err = servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, server.Flavor["id"], choices.FlavorID) +} + +/* +func TestServersActionPause(t *testing.T) { + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + t.Logf("Attempting to pause server %s", server.ID) + err = pauseunpause.Pause(client, server.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = WaitForComputeStatus(client, server, "PAUSED") + th.AssertNoErr(t, err) + + err = pauseunpause.Unpause(client, server.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = WaitForComputeStatus(client, server, "ACTIVE") + th.AssertNoErr(t, err) +} +*/ + +/* +func TestServersActionSuspend(t *testing.T) { + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + t.Logf("Attempting to suspend server %s", server.ID) + err = suspendresume.Suspend(client, server.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = WaitForComputeStatus(client, server, "SUSPENDED") + th.AssertNoErr(t, err) + + err = suspendresume.Resume(client, server.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = WaitForComputeStatus(client, server, "ACTIVE") + th.AssertNoErr(t, err) +} +*/ + +/* +func TestServersActionLock(t *testing.T) { + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + t.Logf("Attempting to Lock server %s", server.ID) + err = lockunlock.Lock(client, server.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = servers.Delete(client, server.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = lockunlock.Unlock(client, server.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = WaitForComputeStatus(client, server, "ACTIVE") + th.AssertNoErr(t, err) +} +*/ diff --git a/acceptance/openstack/compute/v2/tags_test.go b/acceptance/openstack/compute/v2/tags_test.go new file mode 100644 index 000000000..ed04ae79f --- /dev/null +++ b/acceptance/openstack/compute/v2/tags_test.go @@ -0,0 +1,62 @@ +// +build acceptance compute tags + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/tags" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestServersTags(t *testing.T) { + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + tools.PrintResource(t, server) + + err = tags.PutTags(client, server.ID, "tag1", "tag2") + th.AssertNoErr(t, err) + t.Logf("Update tags succcessfully") + + tagList, err := tags.ListTags(client, server.ID).Extract() + th.AssertNoErr(t, err) + + expectedTags := []string{"tag1", "tag2"} + th.AssertDeepEquals(t, expectedTags, tagList.Tags) + + err = tags.DeleteTag(client, server.ID, "tag1") + th.AssertNoErr(t, err) + + tagList, err = tags.ListTags(client, server.ID).Extract() + th.AssertNoErr(t, err) + + expectedTags = []string{"tag2"} + th.AssertDeepEquals(t, expectedTags, tagList.Tags) + + err = tags.AddTag(client, server.ID, "tag1") + th.AssertNoErr(t, err) + t.Logf("Add tag successfully") + + tagList, err = tags.ListTags(client, server.ID).Extract() + th.AssertNoErr(t, err) + + expectedTags = []string{"tag1", "tag2"} + + th.AssertDeepEquals(t, expectedTags, tagList.Tags) + + err = tags.CleanTags(client, server.ID) + th.AssertNoErr(t, err) + t.Logf("Clean tags successfully") + + tagList, err = tags.ListTags(client, server.ID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, map[string]string{}, tagList) +} diff --git a/acceptance/openstack/compute/v2/tenantnetworks_test.go b/acceptance/openstack/compute/v2/tenantnetworks_test.go new file mode 100644 index 000000000..a5a635c11 --- /dev/null +++ b/acceptance/openstack/compute/v2/tenantnetworks_test.go @@ -0,0 +1,53 @@ +// +build acceptance compute servers + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + "github.com/huaweicloud/golangsdk/acceptance/tools" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/tenantnetworks" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestTenantNetworksList(t *testing.T) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + allPages, err := tenantnetworks.List(client).AllPages() + th.AssertNoErr(t, err) + + allTenantNetworks, err := tenantnetworks.ExtractNetworks(allPages) + th.AssertNoErr(t, err) + + var found bool + for _, network := range allTenantNetworks { + tools.PrintResource(t, network) + + if network.Name == choices.NetworkName { + found = true + } + } + + th.AssertEquals(t, found, true) +} + +func TestTenantNetworksGet(t *testing.T) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName) + th.AssertNoErr(t, err) + + network, err := tenantnetworks.Get(client, networkID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, network) +} diff --git a/acceptance/openstack/compute/v2/volumeattach_test.go b/acceptance/openstack/compute/v2/volumeattach_test.go new file mode 100644 index 000000000..601cd3bbd --- /dev/null +++ b/acceptance/openstack/compute/v2/volumeattach_test.go @@ -0,0 +1,38 @@ +// +build acceptance compute volumeattach + +package v2 + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/acceptance/clients" + bs "github.com/huaweicloud/golangsdk/acceptance/openstack/blockstorage/v2" + "github.com/huaweicloud/golangsdk/acceptance/tools" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestVolumeAttachAttachment(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + blockClient, err := clients.NewBlockStorageV2Client() + th.AssertNoErr(t, err) + + server, err := CreateServer(t, client) + th.AssertNoErr(t, err) + defer DeleteServer(t, client, server) + + volume, err := bs.CreateVolume(t, blockClient) + th.AssertNoErr(t, err) + defer bs.DeleteVolume(t, blockClient, volume) + + volumeAttachment, err := CreateVolumeAttachment(t, client, blockClient, server, volume) + th.AssertNoErr(t, err) + defer DeleteVolumeAttachment(t, client, blockClient, server, volumeAttachment) + + tools.PrintResource(t, volumeAttachment) + + th.AssertEquals(t, volumeAttachment.ServerID, server.ID) +} diff --git a/openstack/compute/v2/extensions/attachinterfaces/doc.go b/openstack/compute/v2/extensions/attachinterfaces/doc.go new file mode 100644 index 000000000..3653122bf --- /dev/null +++ b/openstack/compute/v2/extensions/attachinterfaces/doc.go @@ -0,0 +1,52 @@ +/* +Package attachinterfaces provides the ability to retrieve and manage network +interfaces through Nova. + +Example of Listing a Server's Interfaces + + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + allPages, err := attachinterfaces.List(computeClient, serverID).AllPages() + if err != nil { + panic(err) + } + + allInterfaces, err := attachinterfaces.ExtractInterfaces(allPages) + if err != nil { + panic(err) + } + + for _, interface := range allInterfaces { + fmt.Printf("%+v\n", interface) + } + +Example to Get a Server's Interface + + portID = "0dde1598-b374-474e-986f-5b8dd1df1d4e" + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + interface, err := attachinterfaces.Get(computeClient, serverID, portID).Extract() + if err != nil { + panic(err) + } + +Example to Create a new Interface attachment on the Server + + networkID := "8a5fe506-7e9f-4091-899b-96336909d93c" + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + attachOpts := attachinterfaces.CreateOpts{ + NetworkID: networkID, + } + interface, err := attachinterfaces.Create(computeClient, serverID, attachOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete an Interface attachment from the Server + + portID = "0dde1598-b374-474e-986f-5b8dd1df1d4e" + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + err := attachinterfaces.Delete(computeClient, serverID, portID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package attachinterfaces diff --git a/openstack/compute/v2/extensions/attachinterfaces/requests.go b/openstack/compute/v2/extensions/attachinterfaces/requests.go new file mode 100644 index 000000000..fb8bbc6b7 --- /dev/null +++ b/openstack/compute/v2/extensions/attachinterfaces/requests.go @@ -0,0 +1,72 @@ +package attachinterfaces + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// List makes a request against the nova API to list the server's interfaces. +func List(client *golangsdk.ServiceClient, serverID string) pagination.Pager { + return pagination.NewPager(client, listInterfaceURL(client, serverID), func(r pagination.PageResult) pagination.Page { + return InterfacePage{pagination.SinglePageBase(r)} + }) +} + +// Get requests details on a single interface attachment by the server and port IDs. +func Get(client *golangsdk.ServiceClient, serverID, portID string) (r GetResult) { + _, r.Err = client.Get(getInterfaceURL(client, serverID, portID), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToAttachInterfacesCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies parameters of a new interface attachment. +type CreateOpts struct { + + // PortID is the ID of the port for which you want to create an interface. + // The NetworkID and PortID parameters are mutually exclusive. + // If you do not specify the PortID parameter, the OpenStack Networking API + // v2.0 allocates a port and creates an interface for it on the network. + PortID string `json:"port_id,omitempty"` + + // NetworkID is the ID of the network for which you want to create an interface. + // The NetworkID and PortID parameters are mutually exclusive. + // If you do not specify the NetworkID parameter, the OpenStack Networking + // API v2.0 uses the network information cache that is associated with the instance. + NetworkID string `json:"net_id,omitempty"` + + // Slice of FixedIPs. If you request a specific FixedIP address without a + // NetworkID, the request returns a Bad Request (400) response code. + FixedIPs []FixedIP `json:"fixed_ips,omitempty"` +} + +// ToAttachInterfacesCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToAttachInterfacesCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "interfaceAttachment") +} + +// Create requests the creation of a new interface attachment on the server. +func Create(client *golangsdk.ServiceClient, serverID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToAttachInterfacesCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createInterfaceURL(client, serverID), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete makes a request against the nova API to detach a single interface from the server. +// It needs server and port IDs to make a such request. +func Delete(client *golangsdk.ServiceClient, serverID, portID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteInterfaceURL(client, serverID, portID), nil) + return +} diff --git a/openstack/compute/v2/extensions/attachinterfaces/results.go b/openstack/compute/v2/extensions/attachinterfaces/results.go new file mode 100644 index 000000000..414e846a3 --- /dev/null +++ b/openstack/compute/v2/extensions/attachinterfaces/results.go @@ -0,0 +1,78 @@ +package attachinterfaces + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +type attachInterfaceResult struct { + golangsdk.Result +} + +// Extract interprets any attachInterfaceResult as an Interface, if possible. +func (r attachInterfaceResult) Extract() (*Interface, error) { + var s struct { + Interface *Interface `json:"interfaceAttachment"` + } + err := r.ExtractInto(&s) + return s.Interface, err +} + +// GetResult is the response from a Get operation. Call its Extract +// method to interpret it as an Interface. +type GetResult struct { + attachInterfaceResult +} + +// CreateResult is the response from a Create operation. Call its Extract +// method to interpret it as an Interface. +type CreateResult struct { + attachInterfaceResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} + +// FixedIP represents a Fixed IP Address. +type FixedIP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address"` +} + +// Interface represents a network interface on a server. +type Interface struct { + PortState string `json:"port_state"` + FixedIPs []FixedIP `json:"fixed_ips"` + PortID string `json:"port_id"` + NetID string `json:"net_id"` + MACAddr string `json:"mac_addr"` +} + +// InterfacePage abstracts the raw results of making a List() request against +// the API. +// +// As OpenStack extensions may freely alter the response bodies of structures +// returned to the client, you may only safely access the data provided through +// the ExtractInterfaces call. +type InterfacePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if an InterfacePage contains no interfaces. +func (r InterfacePage) IsEmpty() (bool, error) { + interfaces, err := ExtractInterfaces(r) + return len(interfaces) == 0, err +} + +// ExtractInterfaces interprets the results of a single page from a List() call, +// producing a slice of Interface structs. +func ExtractInterfaces(r pagination.Page) ([]Interface, error) { + var s struct { + Interfaces []Interface `json:"interfaceAttachments"` + } + err := (r.(InterfacePage)).ExtractInto(&s) + return s.Interfaces, err +} diff --git a/openstack/compute/v2/extensions/attachinterfaces/testing/doc.go b/openstack/compute/v2/extensions/attachinterfaces/testing/doc.go new file mode 100644 index 000000000..cfc07ad55 --- /dev/null +++ b/openstack/compute/v2/extensions/attachinterfaces/testing/doc.go @@ -0,0 +1,2 @@ +// attachinterfaces unit tests +package testing diff --git a/openstack/compute/v2/extensions/attachinterfaces/testing/fixtures.go b/openstack/compute/v2/extensions/attachinterfaces/testing/fixtures.go new file mode 100644 index 000000000..1178f6fa3 --- /dev/null +++ b/openstack/compute/v2/extensions/attachinterfaces/testing/fixtures.go @@ -0,0 +1,162 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/attachinterfaces" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// ListInterfacesExpected represents an expected repsonse from a ListInterfaces request. +var ListInterfacesExpected = []attachinterfaces.Interface{ + { + PortState: "ACTIVE", + FixedIPs: []attachinterfaces.FixedIP{ + { + SubnetID: "d7906db4-a566-4546-b1f4-5c7fa70f0bf3", + IPAddress: "10.0.0.7", + }, + { + SubnetID: "45906d64-a548-4276-h1f8-kcffa80fjbnl", + IPAddress: "10.0.0.8", + }, + }, + PortID: "0dde1598-b374-474e-986f-5b8dd1df1d4e", + NetID: "8a5fe506-7e9f-4091-899b-96336909d93c", + MACAddr: "fa:16:3e:38:2d:80", + }, +} + +// GetInterfaceExpected represents an expected repsonse from a GetInterface request. +var GetInterfaceExpected = attachinterfaces.Interface{ + PortState: "ACTIVE", + FixedIPs: []attachinterfaces.FixedIP{ + { + SubnetID: "d7906db4-a566-4546-b1f4-5c7fa70f0bf3", + IPAddress: "10.0.0.7", + }, + { + SubnetID: "45906d64-a548-4276-h1f8-kcffa80fjbnl", + IPAddress: "10.0.0.8", + }, + }, + PortID: "0dde1598-b374-474e-986f-5b8dd1df1d4e", + NetID: "8a5fe506-7e9f-4091-899b-96336909d93c", + MACAddr: "fa:16:3e:38:2d:80", +} + +// CreateInterfacesExpected represents an expected repsonse from a CreateInterface request. +var CreateInterfacesExpected = attachinterfaces.Interface{ + PortState: "ACTIVE", + FixedIPs: []attachinterfaces.FixedIP{ + { + SubnetID: "d7906db4-a566-4546-b1f4-5c7fa70f0bf3", + IPAddress: "10.0.0.7", + }, + }, + PortID: "0dde1598-b374-474e-986f-5b8dd1df1d4e", + NetID: "8a5fe506-7e9f-4091-899b-96336909d93c", + MACAddr: "fa:16:3e:38:2d:80", +} + +// HandleInterfaceListSuccessfully sets up the test server to respond to a ListInterfaces request. +func HandleInterfaceListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/b07e7a3b-d951-4efc-a4f9-ac9f001afb7f/os-interface", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "interfaceAttachments": [ + { + "port_state":"ACTIVE", + "fixed_ips": [ + { + "subnet_id": "d7906db4-a566-4546-b1f4-5c7fa70f0bf3", + "ip_address": "10.0.0.7" + }, + { + "subnet_id": "45906d64-a548-4276-h1f8-kcffa80fjbnl", + "ip_address": "10.0.0.8" + } + ], + "port_id": "0dde1598-b374-474e-986f-5b8dd1df1d4e", + "net_id": "8a5fe506-7e9f-4091-899b-96336909d93c", + "mac_addr": "fa:16:3e:38:2d:80" + } + ] + }`) + }) +} + +// HandleInterfaceGetSuccessfully sets up the test server to respond to a GetInterface request. +func HandleInterfaceGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/b07e7a3b-d951-4efc-a4f9-ac9f001afb7f/os-interface/0dde1598-b374-474e-986f-5b8dd1df1d4e", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "interfaceAttachment": + { + "port_state":"ACTIVE", + "fixed_ips": [ + { + "subnet_id": "d7906db4-a566-4546-b1f4-5c7fa70f0bf3", + "ip_address": "10.0.0.7" + }, + { + "subnet_id": "45906d64-a548-4276-h1f8-kcffa80fjbnl", + "ip_address": "10.0.0.8" + } + ], + "port_id": "0dde1598-b374-474e-986f-5b8dd1df1d4e", + "net_id": "8a5fe506-7e9f-4091-899b-96336909d93c", + "mac_addr": "fa:16:3e:38:2d:80" + } + }`) + }) +} + +// HandleInterfaceCreateSuccessfully sets up the test server to respond to a CreateInterface request. +func HandleInterfaceCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/b07e7a3b-d951-4efc-a4f9-ac9f001afb7f/os-interface", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "interfaceAttachment": { + "net_id": "8a5fe506-7e9f-4091-899b-96336909d93c" + } + }`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "interfaceAttachment": + { + "port_state":"ACTIVE", + "fixed_ips": [ + { + "subnet_id": "d7906db4-a566-4546-b1f4-5c7fa70f0bf3", + "ip_address": "10.0.0.7" + } + ], + "port_id": "0dde1598-b374-474e-986f-5b8dd1df1d4e", + "net_id": "8a5fe506-7e9f-4091-899b-96336909d93c", + "mac_addr": "fa:16:3e:38:2d:80" + } + }`) + }) +} + +// HandleInterfaceDeleteSuccessfully sets up the test server to respond to a DeleteInterface request. +func HandleInterfaceDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/b07e7a3b-d951-4efc-a4f9-ac9f001afb7f/os-interface/0dde1598-b374-474e-986f-5b8dd1df1d4e", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/openstack/compute/v2/extensions/attachinterfaces/testing/requests_test.go b/openstack/compute/v2/extensions/attachinterfaces/testing/requests_test.go new file mode 100644 index 000000000..559bfb7b3 --- /dev/null +++ b/openstack/compute/v2/extensions/attachinterfaces/testing/requests_test.go @@ -0,0 +1,89 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/attachinterfaces" + "github.com/huaweicloud/golangsdk/pagination" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func TestListInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleInterfaceListSuccessfully(t) + + expected := ListInterfacesExpected + pages := 0 + err := attachinterfaces.List(client.ServiceClient(), "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f").EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := attachinterfaces.ExtractInterfaces(page) + th.AssertNoErr(t, err) + + if len(actual) != 1 { + t.Fatalf("Expected 1 interface, got %d", len(actual)) + } + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, pages) +} + +func TestListInterfacesAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleInterfaceListSuccessfully(t) + + allPages, err := attachinterfaces.List(client.ServiceClient(), "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f").AllPages() + th.AssertNoErr(t, err) + _, err = attachinterfaces.ExtractInterfaces(allPages) + th.AssertNoErr(t, err) +} + +func TestGetInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleInterfaceGetSuccessfully(t) + + expected := GetInterfaceExpected + + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + interfaceID := "0dde1598-b374-474e-986f-5b8dd1df1d4e" + + actual, err := attachinterfaces.Get(client.ServiceClient(), serverID, interfaceID).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expected, actual) +} + +func TestCreateInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleInterfaceCreateSuccessfully(t) + + expected := CreateInterfacesExpected + + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + networkID := "8a5fe506-7e9f-4091-899b-96336909d93c" + + actual, err := attachinterfaces.Create(client.ServiceClient(), serverID, attachinterfaces.CreateOpts{ + NetworkID: networkID, + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &expected, actual) +} + +func TestDeleteInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleInterfaceDeleteSuccessfully(t) + + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + portID := "0dde1598-b374-474e-986f-5b8dd1df1d4e" + + err := attachinterfaces.Delete(client.ServiceClient(), serverID, portID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/openstack/compute/v2/extensions/attachinterfaces/urls.go b/openstack/compute/v2/extensions/attachinterfaces/urls.go new file mode 100644 index 000000000..9df2021b1 --- /dev/null +++ b/openstack/compute/v2/extensions/attachinterfaces/urls.go @@ -0,0 +1,18 @@ +package attachinterfaces + +import "github.com/huaweicloud/golangsdk" + +func listInterfaceURL(client *golangsdk.ServiceClient, serverID string) string { + return client.ServiceURL("servers", serverID, "os-interface") +} + +func getInterfaceURL(client *golangsdk.ServiceClient, serverID, portID string) string { + return client.ServiceURL("servers", serverID, "os-interface", portID) +} + +func createInterfaceURL(client *golangsdk.ServiceClient, serverID string) string { + return client.ServiceURL("servers", serverID, "os-interface") +} +func deleteInterfaceURL(client *golangsdk.ServiceClient, serverID, portID string) string { + return client.ServiceURL("servers", serverID, "os-interface", portID) +} diff --git a/openstack/compute/v2/extensions/availabilityzones/doc.go b/openstack/compute/v2/extensions/availabilityzones/doc.go new file mode 100644 index 000000000..29b554d21 --- /dev/null +++ b/openstack/compute/v2/extensions/availabilityzones/doc.go @@ -0,0 +1,61 @@ +/* +Package availabilityzones provides the ability to get lists and detailed +availability zone information and to extend a server result with +availability zone information. + +Example of Extend server result with Availability Zone Information: + + type ServerWithAZ struct { + servers.Server + availabilityzones.ServerAvailabilityZoneExt + } + + var allServers []ServerWithAZ + + allPages, err := servers.List(client, nil).AllPages() + if err != nil { + panic("Unable to retrieve servers: %s", err) + } + + err = servers.ExtractServersInto(allPages, &allServers) + if err != nil { + panic("Unable to extract servers: %s", err) + } + + for _, server := range allServers { + fmt.Println(server.AvailabilityZone) + } + +Example of Get Availability Zone Information + + allPages, err := availabilityzones.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } + +Example of Get Detailed Availability Zone Information + + allPages, err := availabilityzones.ListDetail(computeClient).AllPages() + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } +*/ +package availabilityzones diff --git a/openstack/compute/v2/extensions/availabilityzones/requests.go b/openstack/compute/v2/extensions/availabilityzones/requests.go new file mode 100644 index 000000000..61d296794 --- /dev/null +++ b/openstack/compute/v2/extensions/availabilityzones/requests.go @@ -0,0 +1,20 @@ +package availabilityzones + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// List will return the existing availability zones. +func List(client *golangsdk.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} + +// ListDetail will return the existing availability zones with detailed information. +// func ListDetail(client *golangsdk.ServiceClient) pagination.Pager { +// return pagination.NewPager(client, listDetailURL(client), func(r pagination.PageResult) pagination.Page { +// return AvailabilityZonePage{pagination.SinglePageBase(r)} +// }) +// } diff --git a/openstack/compute/v2/extensions/availabilityzones/results.go b/openstack/compute/v2/extensions/availabilityzones/results.go new file mode 100644 index 000000000..bebe4f32b --- /dev/null +++ b/openstack/compute/v2/extensions/availabilityzones/results.go @@ -0,0 +1,76 @@ +package availabilityzones + +import ( + "encoding/json" + "time" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// ServerAvailabilityZoneExt is an extension to the base Server object. +type ServerAvailabilityZoneExt struct { + // AvailabilityZone is the availabilty zone the server is in. + AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` +} + +// ServiceState represents the state of a service in an AvailabilityZone. +type ServiceState struct { + Active bool `json:"active"` + Available bool `json:"available"` + UpdatedAt time.Time `json:"-"` +} + +// UnmarshalJSON to override default +func (r *ServiceState) UnmarshalJSON(b []byte) error { + type tmp ServiceState + var s struct { + tmp + UpdatedAt golangsdk.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ServiceState(s.tmp) + + r.UpdatedAt = time.Time(s.UpdatedAt) + + return nil +} + +// Services is a map of services contained in an AvailabilityZone. +type Services map[string]ServiceState + +// Hosts is map of hosts/nodes contained in an AvailabilityZone. +// Each host can have multiple services. +type Hosts map[string]Services + +// ZoneState represents the current state of the availability zone. +type ZoneState struct { + // Returns true if the availability zone is available + Available bool `json:"available"` +} + +// AvailabilityZone contains all the information associated with an OpenStack +// AvailabilityZone. +type AvailabilityZone struct { + Hosts Hosts `json:"hosts"` + // The availability zone name + ZoneName string `json:"zoneName"` + ZoneState ZoneState `json:"zoneState"` +} + +type AvailabilityZonePage struct { + pagination.SinglePageBase +} + +// ExtractAvailabilityZones returns a slice of AvailabilityZones contained in a +// single page of results. +func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) { + var s struct { + AvailabilityZoneInfo []AvailabilityZone `json:"availabilityZoneInfo"` + } + err := (r.(AvailabilityZonePage)).ExtractInto(&s) + return s.AvailabilityZoneInfo, err +} diff --git a/openstack/compute/v2/extensions/availabilityzones/testing/doc.go b/openstack/compute/v2/extensions/availabilityzones/testing/doc.go new file mode 100644 index 000000000..a4408d7a0 --- /dev/null +++ b/openstack/compute/v2/extensions/availabilityzones/testing/doc.go @@ -0,0 +1,2 @@ +// availabilityzones unittests +package testing diff --git a/openstack/compute/v2/extensions/availabilityzones/testing/fixtures.go b/openstack/compute/v2/extensions/availabilityzones/testing/fixtures.go new file mode 100644 index 000000000..92f02334b --- /dev/null +++ b/openstack/compute/v2/extensions/availabilityzones/testing/fixtures.go @@ -0,0 +1,197 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + az "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/availabilityzones" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +const GetOutput = ` +{ + "availabilityZoneInfo": [ + { + "hosts": null, + "zoneName": "nova", + "zoneState": { + "available": true + } + } + ] +} +` + +const GetDetailOutput = ` +{ + "availabilityZoneInfo": [ + { + "hosts": { + "localhost": { + "nova-cert": { + "active": true, + "available": false, + "updated_at": "2017-10-14T17:03:39.000000" + }, + "nova-conductor": { + "active": true, + "available": false, + "updated_at": "2017-10-14T17:04:09.000000" + }, + "nova-consoleauth": { + "active": true, + "available": false, + "updated_at": "2017-10-14T17:04:18.000000" + }, + "nova-scheduler": { + "active": true, + "available": false, + "updated_at": "2017-10-14T17:04:30.000000" + } + }, + "openstack-acc-tests.novalocal": { + "nova-cert": { + "active": true, + "available": true, + "updated_at": "2018-01-04T04:11:19.000000" + }, + "nova-conductor": { + "active": true, + "available": true, + "updated_at": "2018-01-04T04:11:22.000000" + }, + "nova-consoleauth": { + "active": true, + "available": true, + "updated_at": "2018-01-04T04:11:20.000000" + }, + "nova-scheduler": { + "active": true, + "available": true, + "updated_at": "2018-01-04T04:11:23.000000" + } + } + }, + "zoneName": "internal", + "zoneState": { + "available": true + } + }, + { + "hosts": { + "openstack-acc-tests.novalocal": { + "nova-compute": { + "active": true, + "available": true, + "updated_at": "2018-01-04T04:11:23.000000" + } + } + }, + "zoneName": "nova", + "zoneState": { + "available": true + } + } + ] +}` + +var AZResult = []az.AvailabilityZone{ + { + Hosts: nil, + ZoneName: "nova", + ZoneState: az.ZoneState{Available: true}, + }, +} + +var AZDetailResult = []az.AvailabilityZone{ + { + Hosts: az.Hosts{ + "localhost": az.Services{ + "nova-cert": az.ServiceState{ + Active: true, + Available: false, + UpdatedAt: time.Date(2017, 10, 14, 17, 3, 39, 0, time.UTC), + }, + "nova-conductor": az.ServiceState{ + Active: true, + Available: false, + UpdatedAt: time.Date(2017, 10, 14, 17, 4, 9, 0, time.UTC), + }, + "nova-consoleauth": az.ServiceState{ + Active: true, + Available: false, + UpdatedAt: time.Date(2017, 10, 14, 17, 4, 18, 0, time.UTC), + }, + "nova-scheduler": az.ServiceState{ + Active: true, + Available: false, + UpdatedAt: time.Date(2017, 10, 14, 17, 4, 30, 0, time.UTC), + }, + }, + "openstack-acc-tests.novalocal": az.Services{ + "nova-cert": az.ServiceState{ + Active: true, + Available: true, + UpdatedAt: time.Date(2018, 1, 4, 4, 11, 19, 0, time.UTC), + }, + "nova-conductor": az.ServiceState{ + Active: true, + Available: true, + UpdatedAt: time.Date(2018, 1, 4, 4, 11, 22, 0, time.UTC), + }, + "nova-consoleauth": az.ServiceState{ + Active: true, + Available: true, + UpdatedAt: time.Date(2018, 1, 4, 4, 11, 20, 0, time.UTC), + }, + "nova-scheduler": az.ServiceState{ + Active: true, + Available: true, + UpdatedAt: time.Date(2018, 1, 4, 4, 11, 23, 0, time.UTC), + }, + }, + }, + ZoneName: "internal", + ZoneState: az.ZoneState{Available: true}, + }, + { + Hosts: az.Hosts{ + "openstack-acc-tests.novalocal": az.Services{ + "nova-compute": az.ServiceState{ + Active: true, + Available: true, + UpdatedAt: time.Date(2018, 1, 4, 4, 11, 23, 0, time.UTC), + }, + }, + }, + ZoneName: "nova", + ZoneState: az.ZoneState{Available: true}, + }, +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for availability zone information. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-availability-zone", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleGetDetailSuccessfully configures the test server to respond to a Get request +// for detailed availability zone information. +func HandleGetDetailSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-availability-zone/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetDetailOutput) + }) +} diff --git a/openstack/compute/v2/extensions/availabilityzones/testing/requests_test.go b/openstack/compute/v2/extensions/availabilityzones/testing/requests_test.go new file mode 100644 index 000000000..5b675517e --- /dev/null +++ b/openstack/compute/v2/extensions/availabilityzones/testing/requests_test.go @@ -0,0 +1,41 @@ +package testing + +import ( + "testing" + + az "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/availabilityzones" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// Verifies that availability zones can be listed correctly +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetSuccessfully(t) + + allPages, err := az.List(client.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + + actual, err := az.ExtractAvailabilityZones(allPages) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, AZResult, actual) +} + +// Verifies that detailed availability zones can be listed correctly +// func TestListDetail(t *testing.T) { +// th.SetupHTTP() +// defer th.TeardownHTTP() + +// HandleGetDetailSuccessfully(t) + +// allPages, err := az.ListDetail(client.ServiceClient()).AllPages() +// th.AssertNoErr(t, err) + +// actual, err := az.ExtractAvailabilityZones(allPages) +// th.AssertNoErr(t, err) + +// th.CheckDeepEquals(t, AZDetailResult, actual) +// } diff --git a/openstack/compute/v2/extensions/availabilityzones/urls.go b/openstack/compute/v2/extensions/availabilityzones/urls.go new file mode 100644 index 000000000..30f3aa1af --- /dev/null +++ b/openstack/compute/v2/extensions/availabilityzones/urls.go @@ -0,0 +1,11 @@ +package availabilityzones + +import "github.com/huaweicloud/golangsdk" + +func listURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL("os-availability-zone") +} + +// func listDetailURL(c *golangsdk.ServiceClient) string { +// return c.ServiceURL("os-availability-zone", "detail") +// } diff --git a/openstack/compute/v2/extensions/diskconfig/doc.go b/openstack/compute/v2/extensions/diskconfig/doc.go new file mode 100644 index 000000000..ed9cc6f73 --- /dev/null +++ b/openstack/compute/v2/extensions/diskconfig/doc.go @@ -0,0 +1,46 @@ +/* +Package diskconfig provides information and interaction with the Disk Config +extension that works with the OpenStack Compute service. + +Example of Obtaining the Disk Config of a Server + + type ServerWithDiskConfig { + servers.Server + diskconfig.ServerDiskConfigExt + } + + var allServers []ServerWithDiskConfig + + allPages, err := servers.List(client, nil).AllPages() + if err != nil { + panic("Unable to retrieve servers: %s", err) + } + + err = servers.ExtractServersInto(allPages, &allServers) + if err != nil { + panic("Unable to extract servers: %s", err) + } + + for _, server := range allServers { + fmt.Println(server.DiskConfig) + } + +Example of Creating a Server with Disk Config + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + createOpts := diskconfig.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + DiskConfig: diskconfig.Manual, + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic("Unable to create server: %s", err) + } +*/ +package diskconfig diff --git a/openstack/compute/v2/extensions/diskconfig/requests.go b/openstack/compute/v2/extensions/diskconfig/requests.go new file mode 100644 index 000000000..6f0ba18bc --- /dev/null +++ b/openstack/compute/v2/extensions/diskconfig/requests.go @@ -0,0 +1,106 @@ +package diskconfig + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" +) + +// DiskConfig represents one of the two possible settings for the DiskConfig +// option when creating, rebuilding, or resizing servers: Auto or Manual. +type DiskConfig string + +const ( + // Auto builds a server with a single partition the size of the target flavor + // disk and automatically adjusts the filesystem to fit the entire partition. + // Auto may only be used with images and servers that use a single EXT3 + // partition. + Auto DiskConfig = "AUTO" + + // Manual builds a server using whatever partition scheme and filesystem are + // present in the source image. If the target flavor disk is larger, the + // remaining space is left unpartitioned. This enables images to have non-EXT3 + // filesystems, multiple partitions, and so on, and enables you to manage the + // disk configuration. It also results in slightly shorter boot times. + Manual DiskConfig = "MANUAL" +) + +// CreateOptsExt adds a DiskConfig option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // DiskConfig [optional] controls how the created server's disk is partitioned. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` +} + +// ToServerCreateMap adds the diskconfig option to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if string(opts.DiskConfig) == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} + +// RebuildOptsExt adds a DiskConfig option to the base RebuildOpts. +type RebuildOptsExt struct { + servers.RebuildOptsBuilder + + // DiskConfig controls how the rebuilt server's disk is partitioned. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` +} + +// ToServerRebuildMap adds the diskconfig option to the base server rebuild options. +func (opts RebuildOptsExt) ToServerRebuildMap() (map[string]interface{}, error) { + if opts.DiskConfig != Auto && opts.DiskConfig != Manual { + err := golangsdk.ErrInvalidInput{} + err.Argument = "diskconfig.RebuildOptsExt.DiskConfig" + err.Info = "Must be either diskconfig.Auto or diskconfig.Manual" + return nil, err + } + + base, err := opts.RebuildOptsBuilder.ToServerRebuildMap() + if err != nil { + return nil, err + } + + serverMap := base["rebuild"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} + +// ResizeOptsExt adds a DiskConfig option to the base server resize options. +type ResizeOptsExt struct { + servers.ResizeOptsBuilder + + // DiskConfig [optional] controls how the resized server's disk is partitioned. + DiskConfig DiskConfig +} + +// ToServerResizeMap adds the diskconfig option to the base server creation options. +func (opts ResizeOptsExt) ToServerResizeMap() (map[string]interface{}, error) { + if opts.DiskConfig != Auto && opts.DiskConfig != Manual { + err := golangsdk.ErrInvalidInput{} + err.Argument = "diskconfig.ResizeOptsExt.DiskConfig" + err.Info = "Must be either diskconfig.Auto or diskconfig.Manual" + return nil, err + } + + base, err := opts.ResizeOptsBuilder.ToServerResizeMap() + if err != nil { + return nil, err + } + + serverMap := base["resize"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} diff --git a/openstack/compute/v2/extensions/diskconfig/results.go b/openstack/compute/v2/extensions/diskconfig/results.go new file mode 100644 index 000000000..239b2683d --- /dev/null +++ b/openstack/compute/v2/extensions/diskconfig/results.go @@ -0,0 +1,6 @@ +package diskconfig + +type ServerDiskConfigExt struct { + // DiskConfig is the disk configuration of the server. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig"` +} diff --git a/openstack/compute/v2/extensions/diskconfig/testing/doc.go b/openstack/compute/v2/extensions/diskconfig/testing/doc.go new file mode 100644 index 000000000..52ab24756 --- /dev/null +++ b/openstack/compute/v2/extensions/diskconfig/testing/doc.go @@ -0,0 +1,2 @@ +// diskconfig unit tests +package testing diff --git a/openstack/compute/v2/extensions/diskconfig/testing/requests_test.go b/openstack/compute/v2/extensions/diskconfig/testing/requests_test.go new file mode 100644 index 000000000..9b5c6edaf --- /dev/null +++ b/openstack/compute/v2/extensions/diskconfig/testing/requests_test.go @@ -0,0 +1,88 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/diskconfig" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := diskconfig.CreateOptsExt{ + CreateOptsBuilder: base, + DiskConfig: diskconfig.Manual, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "OS-DCF:diskConfig": "MANUAL" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestRebuildOpts(t *testing.T) { + base := servers.RebuildOpts{ + Name: "rebuiltserver", + AdminPass: "swordfish", + ImageID: "asdfasdfasdf", + } + + ext := diskconfig.RebuildOptsExt{ + RebuildOptsBuilder: base, + DiskConfig: diskconfig.Auto, + } + + actual, err := ext.ToServerRebuildMap() + th.AssertNoErr(t, err) + + expected := ` + { + "rebuild": { + "name": "rebuiltserver", + "imageRef": "asdfasdfasdf", + "adminPass": "swordfish", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} + +func TestResizeOpts(t *testing.T) { + base := servers.ResizeOpts{ + FlavorRef: "performance1-8", + } + + ext := diskconfig.ResizeOptsExt{ + ResizeOptsBuilder: base, + DiskConfig: diskconfig.Auto, + } + + actual, err := ext.ToServerResizeMap() + th.AssertNoErr(t, err) + + expected := ` + { + "resize": { + "flavorRef": "performance1-8", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} diff --git a/openstack/compute/v2/extensions/extendedstatus/doc.go b/openstack/compute/v2/extensions/extendedstatus/doc.go new file mode 100644 index 000000000..33b1e35cd --- /dev/null +++ b/openstack/compute/v2/extensions/extendedstatus/doc.go @@ -0,0 +1,28 @@ +/* +Package extendedstatus provides the ability to extend a server result with +the extended status information. Example: + + type ServerWithExt struct { + servers.Server + extendedstatus.ServerExtendedStatusExt + } + + var allServers []ServerWithExt + + allPages, err := servers.List(client, nil).AllPages() + if err != nil { + panic("Unable to retrieve servers: %s", err) + } + + err = servers.ExtractServersInto(allPages, &allServers) + if err != nil { + panic("Unable to extract servers: %s", err) + } + + for _, server := range allServers { + fmt.Println(server.TaskState) + fmt.Println(server.VmState) + fmt.Println(server.PowerState) + } +*/ +package extendedstatus diff --git a/openstack/compute/v2/extensions/extendedstatus/results.go b/openstack/compute/v2/extensions/extendedstatus/results.go new file mode 100644 index 000000000..acfbd3fb2 --- /dev/null +++ b/openstack/compute/v2/extensions/extendedstatus/results.go @@ -0,0 +1,41 @@ +package extendedstatus + +type PowerState int + +type ServerExtendedStatusExt struct { + TaskState string `json:"OS-EXT-STS:task_state"` + VmState string `json:"OS-EXT-STS:vm_state"` + PowerState PowerState `json:"OS-EXT-STS:power_state"` +} + +const ( + NOSTATE = iota + RUNNING + _UNUSED1 + PAUSED + SHUTDOWN + _UNUSED2 + CRASHED + SUSPENDED +) + +func (r PowerState) String() string { + switch r { + case NOSTATE: + return "NOSTATE" + case RUNNING: + return "RUNNING" + case PAUSED: + return "PAUSED" + case SHUTDOWN: + return "SHUTDOWN" + case CRASHED: + return "CRASHED" + case SUSPENDED: + return "SUSPENDED" + case _UNUSED1, _UNUSED2: + return "_UNUSED" + default: + return "N/A" + } +} diff --git a/openstack/compute/v2/extensions/floatingips/doc.go b/openstack/compute/v2/extensions/floatingips/doc.go new file mode 100644 index 000000000..f5dbdbf8b --- /dev/null +++ b/openstack/compute/v2/extensions/floatingips/doc.go @@ -0,0 +1,68 @@ +/* +Package floatingips provides the ability to manage floating ips through the +Nova API. + +This API has been deprecated and will be removed from a future release of the +Nova API service. + +For environements that support this extension, this package can be used +regardless of if either Neutron or nova-network is used as the cloud's network +service. + +Example to List Floating IPs + + allPages, err := floatingips.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + allFloatingIPs, err := floatingips.ExtractFloatingIPs(allPages) + if err != nil { + panic(err) + } + + for _, fip := range allFloatingIPs { + fmt.Printf("%+v\n", fip) + } + +Example to Create a Floating IP + + createOpts := floatingips.CreateOpts{ + Pool: "nova", + } + + fip, err := floatingips.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Floating IP + + err := floatingips.Delete(computeClient, "floatingip-id").ExtractErr() + if err != nil { + panic(err) + } + +Example to Associate a Floating IP With a Server + + associateOpts := floatingips.AssociateOpts{ + FloatingIP: "10.10.10.2", + } + + err := floatingips.AssociateInstance(computeClient, "server-id", associateOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Disassociate a Floating IP From a Server + + disassociateOpts := floatingips.DisassociateOpts{ + FloatingIP: "10.10.10.2", + } + + err := floatingips.DisassociateInstance(computeClient, "server-id", disassociateOpts).ExtractErr() + if err != nil { + panic(err) + } +*/ +package floatingips diff --git a/openstack/compute/v2/extensions/floatingips/requests.go b/openstack/compute/v2/extensions/floatingips/requests.go new file mode 100644 index 000000000..a46f34652 --- /dev/null +++ b/openstack/compute/v2/extensions/floatingips/requests.go @@ -0,0 +1,114 @@ +package floatingips + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of FloatingIPs. +func List(client *golangsdk.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return FloatingIPPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToFloatingIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies a Floating IP allocation request. +type CreateOpts struct { + // Pool is the pool of Floating IPs to allocate one from. + Pool string `json:"pool" required:"true"` +} + +// ToFloatingIPCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Create requests the creation of a new Floating IP. +func Create(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFloatingIPCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns data about a previously created Floating IP. +func Get(client *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous allocated Floating IP. +func Delete(client *golangsdk.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// AssociateOptsBuilder allows extensions to add additional parameters to the +// Associate request. +type AssociateOptsBuilder interface { + ToFloatingIPAssociateMap() (map[string]interface{}, error) +} + +// AssociateOpts specifies the required information to associate a Floating IP with an instance +type AssociateOpts struct { + // FloatingIP is the Floating IP to associate with an instance. + FloatingIP string `json:"address" required:"true"` + + // FixedIP is an optional fixed IP address of the server. + FixedIP string `json:"fixed_address,omitempty"` +} + +// ToFloatingIPAssociateMap constructs a request body from AssociateOpts. +func (opts AssociateOpts) ToFloatingIPAssociateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "addFloatingIp") +} + +// AssociateInstance pairs an allocated Floating IP with a server. +func AssociateInstance(client *golangsdk.ServiceClient, serverID string, opts AssociateOptsBuilder) (r AssociateResult) { + b, err := opts.ToFloatingIPAssociateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(associateURL(client, serverID), b, nil, nil) + return +} + +// DisassociateOptsBuilder allows extensions to add additional parameters to +// the Disassociate request. +type DisassociateOptsBuilder interface { + ToFloatingIPDisassociateMap() (map[string]interface{}, error) +} + +// DisassociateOpts specifies the required information to disassociate a +// Floating IP with a server. +type DisassociateOpts struct { + FloatingIP string `json:"address" required:"true"` +} + +// ToFloatingIPDisassociateMap constructs a request body from DisassociateOpts. +func (opts DisassociateOpts) ToFloatingIPDisassociateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "removeFloatingIp") +} + +// DisassociateInstance decouples an allocated Floating IP from an instance +func DisassociateInstance(client *golangsdk.ServiceClient, serverID string, opts DisassociateOptsBuilder) (r DisassociateResult) { + b, err := opts.ToFloatingIPDisassociateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(disassociateURL(client, serverID), b, nil, nil) + return +} diff --git a/openstack/compute/v2/extensions/floatingips/results.go b/openstack/compute/v2/extensions/floatingips/results.go new file mode 100644 index 000000000..8589901d3 --- /dev/null +++ b/openstack/compute/v2/extensions/floatingips/results.go @@ -0,0 +1,115 @@ +package floatingips + +import ( + "encoding/json" + "strconv" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// A FloatingIP is an IP that can be associated with a server. +type FloatingIP struct { + // ID is a unique ID of the Floating IP + ID string `json:"-"` + + // FixedIP is a specific IP on the server to pair the Floating IP with. + FixedIP string `json:"fixed_ip,omitempty"` + + // InstanceID is the ID of the server that is using the Floating IP. + InstanceID string `json:"instance_id"` + + // IP is the actual Floating IP. + IP string `json:"ip"` + + // Pool is the pool of Floating IPs that this Floating IP belongs to. + Pool string `json:"pool"` +} + +func (r *FloatingIP) UnmarshalJSON(b []byte) error { + type tmp FloatingIP + var s struct { + tmp + ID interface{} `json:"id"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = FloatingIP(s.tmp) + + switch t := s.ID.(type) { + case float64: + r.ID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ID = t + } + + return err +} + +// FloatingIPPage stores a single page of FloatingIPs from a List call. +type FloatingIPPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a FloatingIPsPage is empty. +func (page FloatingIPPage) IsEmpty() (bool, error) { + va, err := ExtractFloatingIPs(page) + return len(va) == 0, err +} + +// ExtractFloatingIPs interprets a page of results as a slice of FloatingIPs. +func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) { + var s struct { + FloatingIPs []FloatingIP `json:"floating_ips"` + } + err := (r.(FloatingIPPage)).ExtractInto(&s) + return s.FloatingIPs, err +} + +// FloatingIPResult is the raw result from a FloatingIP request. +type FloatingIPResult struct { + golangsdk.Result +} + +// Extract is a method that attempts to interpret any FloatingIP resource +// response as a FloatingIP struct. +func (r FloatingIPResult) Extract() (*FloatingIP, error) { + var s struct { + FloatingIP *FloatingIP `json:"floating_ip"` + } + err := r.ExtractInto(&s) + return s.FloatingIP, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a FloatingIP. +type CreateResult struct { + FloatingIPResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a FloatingIP. +type GetResult struct { + FloatingIPResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} + +// AssociateResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type AssociateResult struct { + golangsdk.ErrResult +} + +// DisassociateResult is the response from a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DisassociateResult struct { + golangsdk.ErrResult +} diff --git a/openstack/compute/v2/extensions/floatingips/testing/doc.go b/openstack/compute/v2/extensions/floatingips/testing/doc.go new file mode 100644 index 000000000..82dfbe7fe --- /dev/null +++ b/openstack/compute/v2/extensions/floatingips/testing/doc.go @@ -0,0 +1,2 @@ +// floatingips unit tests +package testing diff --git a/openstack/compute/v2/extensions/floatingips/testing/fixtures.go b/openstack/compute/v2/extensions/floatingips/testing/fixtures.go new file mode 100644 index 000000000..e84a8f724 --- /dev/null +++ b/openstack/compute/v2/extensions/floatingips/testing/fixtures.go @@ -0,0 +1,223 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/floatingips" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "floating_ips": [ + { + "fixed_ip": null, + "id": "1", + "instance_id": null, + "ip": "10.10.10.1", + "pool": "nova" + }, + { + "fixed_ip": "166.78.185.201", + "id": "2", + "instance_id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "ip": "10.10.10.2", + "pool": "nova" + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "floating_ip": { + "fixed_ip": "166.78.185.201", + "id": "2", + "instance_id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "ip": "10.10.10.2", + "pool": "nova" + } +} +` + +// CreateOutput is a sample response to a Post call +const CreateOutput = ` +{ + "floating_ip": { + "fixed_ip": null, + "id": "1", + "instance_id": null, + "ip": "10.10.10.1", + "pool": "nova" + } +} +` + +// CreateOutputWithNumericID is a sample response to a Post call +// with a legacy nova-network-based numeric ID. +const CreateOutputWithNumericID = ` +{ + "floating_ip": { + "fixed_ip": null, + "id": 1, + "instance_id": null, + "ip": "10.10.10.1", + "pool": "nova" + } +} +` + +// FirstFloatingIP is the first result in ListOutput. +var FirstFloatingIP = floatingips.FloatingIP{ + ID: "1", + IP: "10.10.10.1", + Pool: "nova", +} + +// SecondFloatingIP is the first result in ListOutput. +var SecondFloatingIP = floatingips.FloatingIP{ + FixedIP: "166.78.185.201", + ID: "2", + InstanceID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + IP: "10.10.10.2", + Pool: "nova", +} + +// ExpectedFloatingIPsSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedFloatingIPsSlice = []floatingips.FloatingIP{FirstFloatingIP, SecondFloatingIP} + +// CreatedFloatingIP is the parsed result from CreateOutput. +var CreatedFloatingIP = floatingips.FloatingIP{ + ID: "1", + IP: "10.10.10.1", + Pool: "nova", +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing floating ip +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips/2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request +// for a new floating ip +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "pool": "nova" +} +`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleCreateWithNumericIDSuccessfully configures the test server to respond to a Create request +// for a new floating ip +func HandleCreateWithNumericIDSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "pool": "nova" +} +`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutputWithNumericID) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// an existing floating ip +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips/1", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleAssociateSuccessfully configures the test server to respond to a Post request +// to associate an allocated floating IP +func HandleAssociateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "addFloatingIp": { + "address": "10.10.10.2" + } +} +`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleFixedAssociateSucessfully configures the test server to respond to a Post request +// to associate an allocated floating IP with a specific fixed IP address +func HandleAssociateFixedSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "addFloatingIp": { + "address": "10.10.10.2", + "fixed_address": "166.78.185.201" + } +} +`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleDisassociateSuccessfully configures the test server to respond to a Post request +// to disassociate an allocated floating IP +func HandleDisassociateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "removeFloatingIp": { + "address": "10.10.10.2" + } +} +`) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/openstack/compute/v2/extensions/floatingips/testing/requests_test.go b/openstack/compute/v2/extensions/floatingips/testing/requests_test.go new file mode 100644 index 000000000..400006466 --- /dev/null +++ b/openstack/compute/v2/extensions/floatingips/testing/requests_test.go @@ -0,0 +1,111 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/floatingips" + "github.com/huaweicloud/golangsdk/pagination" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := floatingips.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := floatingips.ExtractFloatingIPs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedFloatingIPsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + actual, err := floatingips.Create(client.ServiceClient(), floatingips.CreateOpts{ + Pool: "nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedFloatingIP, actual) +} + +func TestCreateWithNumericID(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateWithNumericIDSuccessfully(t) + + actual, err := floatingips.Create(client.ServiceClient(), floatingips.CreateOpts{ + Pool: "nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedFloatingIP, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := floatingips.Get(client.ServiceClient(), "2").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondFloatingIP, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := floatingips.Delete(client.ServiceClient(), "1").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAssociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAssociateSuccessfully(t) + + associateOpts := floatingips.AssociateOpts{ + FloatingIP: "10.10.10.2", + } + + err := floatingips.AssociateInstance(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0", associateOpts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAssociateFixed(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAssociateFixedSuccessfully(t) + + associateOpts := floatingips.AssociateOpts{ + FloatingIP: "10.10.10.2", + FixedIP: "166.78.185.201", + } + + err := floatingips.AssociateInstance(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0", associateOpts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDisassociateInstance(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDisassociateSuccessfully(t) + + disassociateOpts := floatingips.DisassociateOpts{ + FloatingIP: "10.10.10.2", + } + + err := floatingips.DisassociateInstance(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0", disassociateOpts).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/openstack/compute/v2/extensions/floatingips/urls.go b/openstack/compute/v2/extensions/floatingips/urls.go new file mode 100644 index 000000000..efec755a0 --- /dev/null +++ b/openstack/compute/v2/extensions/floatingips/urls.go @@ -0,0 +1,37 @@ +package floatingips + +import "github.com/huaweicloud/golangsdk" + +const resourcePath = "os-floating-ips" + +func resourceURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *golangsdk.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *golangsdk.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func deleteURL(c *golangsdk.ServiceClient, id string) string { + return getURL(c, id) +} + +func serverURL(c *golangsdk.ServiceClient, serverID string) string { + return c.ServiceURL("servers/" + serverID + "/action") +} + +func associateURL(c *golangsdk.ServiceClient, serverID string) string { + return serverURL(c, serverID) +} + +func disassociateURL(c *golangsdk.ServiceClient, serverID string) string { + return serverURL(c, serverID) +} diff --git a/openstack/compute/v2/extensions/keypairs/doc.go b/openstack/compute/v2/extensions/keypairs/doc.go new file mode 100644 index 000000000..8e3c9bcc8 --- /dev/null +++ b/openstack/compute/v2/extensions/keypairs/doc.go @@ -0,0 +1,71 @@ +/* +Package keypairs provides the ability to manage key pairs as well as create +servers with a specified key pair. + +Example to List Key Pairs + + allPages, err := keypairs.List(computeClient, nil).AllPages() + if err != nil { + panic(err) + } + + allKeyPairs, err := keypairs.ExtractKeyPairs(allPages) + if err != nil { + panic(err) + } + + for _, kp := range allKeyPairs { + fmt.Printf("%+v\n", kp) + } + +Example to Create a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + } + + keypair, err := keypairs.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", keypair) + +Example to Import a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + PublicKey: "public-key", + } + + keypair, err := keypairs.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Key Pair + + err := keypairs.Delete(computeClient, "keypair-name").ExtractErr() + if err != nil { + panic(err) + } + +Example to Create a Server With a Key Pair + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + createOpts := keypairs.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + KeyName: "keypair-name", + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } +*/ +package keypairs diff --git a/openstack/compute/v2/extensions/keypairs/requests.go b/openstack/compute/v2/extensions/keypairs/requests.go new file mode 100644 index 000000000..b84fa1101 --- /dev/null +++ b/openstack/compute/v2/extensions/keypairs/requests.go @@ -0,0 +1,123 @@ +package keypairs + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + "github.com/huaweicloud/golangsdk/pagination" +) + +// CreateOptsExt adds a KeyPair option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // KeyName is the name of the key pair. + KeyName string `json:"key_name,omitempty"` +} + +// ToServerCreateMap adds the key_name to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if opts.KeyName == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["key_name"] = opts.KeyName + + return base, nil +} + +// ListOptsBuilder allows extensions to add additional parameters to the list +// request. +type ListOptsBuilder interface { + ToKeypairListQuery() (string, error) +} + +// ListOpts filters the results returned by the List() function. +type ListOpts struct { + // Limit instructs List to refrain from sending excessively large lists. + Limit int `q:"limit"` + // Marker represents a keypair name which the list would begin from. + Marker string `q:"marker"` +} + +// ToKeypairListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToKeypairListQuery() (string, error) { + q, err := golangsdk.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *golangsdk.ServiceClient, opts ListOptsBuilder) pagination.Pager { + + url := listURL(client) + if opts != nil { + query, err := opts.ToKeypairListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return KeyPairPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToKeyPairCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies KeyPair creation or import parameters. +type CreateOpts struct { + // Name is a friendly name to refer to this KeyPair in other services. + Name string `json:"name" required:"true"` + + // PublicKey [optional] is a pregenerated OpenSSH-formatted public key. + // If provided, this key will be imported and no new key will be created. + PublicKey string `json:"public_key,omitempty"` + + // Type is the key pair type normally should be one of ssh or x509. Since + // version 2.2 + Type string `json:"type,omitempty"` + + // UserID is the user id of this key. Since version 2.10 + UserID string `json:"user_id,omitempty"` +} + +// ToKeyPairCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "keypair") +} + +// Create requests the creation of a new KeyPair on the server, or to import a +// pre-existing keypair. +func Create(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToKeyPairCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *golangsdk.ServiceClient, name string) (r GetResult) { + _, r.Err = client.Get(getURL(client, name), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *golangsdk.ServiceClient, name string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, name), nil) + return +} diff --git a/openstack/compute/v2/extensions/keypairs/results.go b/openstack/compute/v2/extensions/keypairs/results.go new file mode 100644 index 000000000..a3e6c28da --- /dev/null +++ b/openstack/compute/v2/extensions/keypairs/results.go @@ -0,0 +1,101 @@ +package keypairs + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// KeyPair is an SSH key known to the OpenStack Cloud that is available to be +// injected into servers. +type KeyPair struct { + // PublicKey is the public key from this pair, in OpenSSH format. + // "ssh-rsa AAAAB3Nz..." + PublicKey string `json:"public_key"` + + // Name is used to refer to this keypair from other services within this + // region. + Name string `json:"name"` + + // Fingerprint is a short sequence of bytes that can be used to authenticate + // or validate a longer public key. + Fingerprint string `json:"fingerprint"` + + // CreateAt is a date of this key has been created + CreatedAt string `json:"created_at"` + + // Deleted is a flag whether this key has been deleted or not + Deleted bool `json:"deleted"` + + // DeletedAt is a date of this key has been deleted + DeletedAt string `json:"deleted_at"` + + // ID is the id of this key + ID string `json:"id"` + + // UpdatedAt is the last updated date of this key + UpdatedAt string `json:"updated_at"` + + // UserID is the user who owns this KeyPair. + UserID string `json:"user_id"` +} + +// KeyPairPage stores a single page of all KeyPair results from a List call. +// Use the ExtractKeyPairs function to convert the results to a slice of +// KeyPairs. +type KeyPairPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a KeyPairPage is empty. +func (page KeyPairPage) IsEmpty() (bool, error) { + ks, err := ExtractKeyPairs(page) + return len(ks) == 0, err +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(r pagination.Page) ([]KeyPair, error) { + type pair struct { + KeyPair KeyPair `json:"keypair"` + } + var s struct { + KeyPairs []pair `json:"keypairs"` + } + err := (r.(KeyPairPage)).ExtractInto(&s) + results := make([]KeyPair, len(s.KeyPairs)) + for i, pair := range s.KeyPairs { + results[i] = pair.KeyPair + } + return results, err +} + +type keyPairResult struct { + golangsdk.Result +} + +// Extract is a method that attempts to interpret any KeyPair resource response +// as a KeyPair struct. +func (r keyPairResult) Extract() (*KeyPair, error) { + var s struct { + KeyPair *KeyPair `json:"keypair"` + } + err := r.ExtractInto(&s) + return s.KeyPair, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a KeyPair. +type CreateResult struct { + keyPairResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a KeyPair. +type GetResult struct { + keyPairResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} diff --git a/openstack/compute/v2/extensions/keypairs/testing/doc.go b/openstack/compute/v2/extensions/keypairs/testing/doc.go new file mode 100644 index 000000000..8d4200983 --- /dev/null +++ b/openstack/compute/v2/extensions/keypairs/testing/doc.go @@ -0,0 +1,2 @@ +// keypairs unit tests +package testing diff --git a/openstack/compute/v2/extensions/keypairs/testing/fixtures.go b/openstack/compute/v2/extensions/keypairs/testing/fixtures.go new file mode 100644 index 000000000..54b725d45 --- /dev/null +++ b/openstack/compute/v2/extensions/keypairs/testing/fixtures.go @@ -0,0 +1,169 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/keypairs" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "keypairs": [ + { + "keypair": { + "fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + "name": "firstkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n" + } + }, + { + "keypair": { + "fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + "name": "secondkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n" + } + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "keypair": { + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", + "name": "firstkey", + "fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a" + } +} +` + +// CreateOutput is a sample response to a Create call. +const CreateOutput = ` +{ + "keypair": { + "fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + "name": "createdkey", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + "user_id": "fake" + } +} +` + +// ImportOutput is a sample response to a Create call that provides its own public key. +const ImportOutput = ` +{ + "keypair": { + "fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + "user_id": "fake" + } +} +` + +// FirstKeyPair is the first result in ListOutput. +var FirstKeyPair = keypairs.KeyPair{ + Name: "firstkey", + Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", +} + +// SecondKeyPair is the second result in ListOutput. +var SecondKeyPair = keypairs.KeyPair{ + Name: "secondkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", +} + +// ExpectedKeyPairSlice is the slice of results that should be parsed from ListOutput, in the expected +// order. +var ExpectedKeyPairSlice = []keypairs.KeyPair{FirstKeyPair, SecondKeyPair} + +// CreatedKeyPair is the parsed result from CreatedOutput. +var CreatedKeyPair = keypairs.KeyPair{ + Name: "createdkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + UserID: "fake", +} + +// ImportedKeyPair is the parsed result from ImportOutput. +var ImportedKeyPair = keypairs.KeyPair{ + Name: "importedkey", + Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + UserID: "fake", +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request for "firstkey". +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs/firstkey", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request for a new +// keypair called "createdkey". +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "keypair": { "name": "createdkey" } }`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleImportSuccessfully configures the test server to respond to an Import request for an +// existing keypair called "importedkey". +func HandleImportSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "keypair": { + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ImportOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// keypair called "deletedkey". +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs/deletedkey", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/openstack/compute/v2/extensions/keypairs/testing/requests_test.go b/openstack/compute/v2/extensions/keypairs/testing/requests_test.go new file mode 100644 index 000000000..99d6a47bf --- /dev/null +++ b/openstack/compute/v2/extensions/keypairs/testing/requests_test.go @@ -0,0 +1,72 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/keypairs" + "github.com/huaweicloud/golangsdk/pagination" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := keypairs.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := keypairs.ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + actual, err := keypairs.Create(client.ServiceClient(), keypairs.CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedKeyPair, actual) +} + +func TestImport(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleImportSuccessfully(t) + + actual, err := keypairs.Create(client.ServiceClient(), keypairs.CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &ImportedKeyPair, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := keypairs.Get(client.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstKeyPair, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := keypairs.Delete(client.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/openstack/compute/v2/extensions/keypairs/urls.go b/openstack/compute/v2/extensions/keypairs/urls.go new file mode 100644 index 000000000..731cf0976 --- /dev/null +++ b/openstack/compute/v2/extensions/keypairs/urls.go @@ -0,0 +1,25 @@ +package keypairs + +import "github.com/huaweicloud/golangsdk" + +const resourcePath = "os-keypairs" + +func resourceURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *golangsdk.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *golangsdk.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *golangsdk.ServiceClient, name string) string { + return c.ServiceURL(resourcePath, name) +} + +func deleteURL(c *golangsdk.ServiceClient, name string) string { + return getURL(c, name) +} diff --git a/openstack/compute/v2/extensions/schedulerhints/doc.go b/openstack/compute/v2/extensions/schedulerhints/doc.go new file mode 100644 index 000000000..2d9d3acde --- /dev/null +++ b/openstack/compute/v2/extensions/schedulerhints/doc.go @@ -0,0 +1,76 @@ +/* +Package schedulerhints extends the server create request with the ability to +specify additional parameters which determine where the server will be +created in the OpenStack cloud. + +Example to Add a Server to a Server Group + + schedulerHints := schedulerhints.SchedulerHints{ + Group: "servergroup-uuid", + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + createOpts := schedulerhints.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + SchedulerHints: schedulerHints, + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Place Server B on a Different Host than Server A + + schedulerHints := schedulerhints.SchedulerHints{ + DifferentHost: []string{ + "server-a-uuid", + } + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_b", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + createOpts := schedulerhints.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + SchedulerHints: schedulerHints, + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Place Server B on the Same Host as Server A + + schedulerHints := schedulerhints.SchedulerHints{ + SameHost: []string{ + "server-a-uuid", + } + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_b", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + createOpts := schedulerhints.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + SchedulerHints: schedulerHints, + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } +*/ +package schedulerhints diff --git a/openstack/compute/v2/extensions/schedulerhints/requests.go b/openstack/compute/v2/extensions/schedulerhints/requests.go new file mode 100644 index 000000000..9a6d65527 --- /dev/null +++ b/openstack/compute/v2/extensions/schedulerhints/requests.go @@ -0,0 +1,164 @@ +package schedulerhints + +import ( + "net" + "regexp" + "strings" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" +) + +// SchedulerHints represents a set of scheduling hints that are passed to the +// OpenStack scheduler. +type SchedulerHints struct { + // Group specifies a Server Group to place the instance in. + Group string + + // DifferentHost will place the instance on a compute node that does not + // host the given instances. + DifferentHost []string + + // SameHost will place the instance on a compute node that hosts the given + // instances. + SameHost []string + + // Query is a conditional statement that results in compute nodes able to + // host the instance. + Query []interface{} + + // TargetCell specifies a cell name where the instance will be placed. + TargetCell string `json:"target_cell,omitempty"` + + // BuildNearHostIP specifies a subnet of compute nodes to host the instance. + BuildNearHostIP string + + // AdditionalProperies are arbitrary key/values that are not validated by nova. + AdditionalProperties map[string]interface{} +} + +// CreateOptsBuilder builds the scheduler hints into a serializable format. +type CreateOptsBuilder interface { + ToServerSchedulerHintsCreateMap() (map[string]interface{}, error) +} + +// ToServerSchedulerHintsMap builds the scheduler hints into a serializable format. +func (opts SchedulerHints) ToServerSchedulerHintsCreateMap() (map[string]interface{}, error) { + sh := make(map[string]interface{}) + + uuidRegex, _ := regexp.Compile("^[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$") + + if opts.Group != "" { + if !uuidRegex.MatchString(opts.Group) { + err := golangsdk.ErrInvalidInput{} + err.Argument = "schedulerhints.SchedulerHints.Group" + err.Value = opts.Group + err.Info = "Group must be a UUID" + return nil, err + } + sh["group"] = opts.Group + } + + if len(opts.DifferentHost) > 0 { + for _, diffHost := range opts.DifferentHost { + if !uuidRegex.MatchString(diffHost) { + err := golangsdk.ErrInvalidInput{} + err.Argument = "schedulerhints.SchedulerHints.DifferentHost" + err.Value = opts.DifferentHost + err.Info = "The hosts must be in UUID format." + return nil, err + } + } + sh["different_host"] = opts.DifferentHost + } + + if len(opts.SameHost) > 0 { + for _, sameHost := range opts.SameHost { + if !uuidRegex.MatchString(sameHost) { + err := golangsdk.ErrInvalidInput{} + err.Argument = "schedulerhints.SchedulerHints.SameHost" + err.Value = opts.SameHost + err.Info = "The hosts must be in UUID format." + return nil, err + } + } + sh["same_host"] = opts.SameHost + } + + /* + Query can be something simple like: + [">=", "$free_ram_mb", 1024] + + Or more complex like: + ['and', + ['>=', '$free_ram_mb', 1024], + ['>=', '$free_disk_mb', 200 * 1024] + ] + + Because of the possible complexity, just make sure the length is a minimum of 3. + */ + if len(opts.Query) > 0 { + if len(opts.Query) < 3 { + err := golangsdk.ErrInvalidInput{} + err.Argument = "schedulerhints.SchedulerHints.Query" + err.Value = opts.Query + err.Info = "Must be a conditional statement in the format of [op,variable,value]" + return nil, err + } + sh["query"] = opts.Query + } + + if opts.TargetCell != "" { + sh["target_cell"] = opts.TargetCell + } + + if opts.BuildNearHostIP != "" { + if _, _, err := net.ParseCIDR(opts.BuildNearHostIP); err != nil { + err := golangsdk.ErrInvalidInput{} + err.Argument = "schedulerhints.SchedulerHints.BuildNearHostIP" + err.Value = opts.BuildNearHostIP + err.Info = "Must be a valid subnet in the form 192.168.1.1/24" + return nil, err + } + ipParts := strings.Split(opts.BuildNearHostIP, "/") + sh["build_near_host_ip"] = ipParts[0] + sh["cidr"] = "/" + ipParts[1] + } + + if opts.AdditionalProperties != nil { + for k, v := range opts.AdditionalProperties { + sh[k] = v + } + } + + return sh, nil +} + +// CreateOptsExt adds a SchedulerHints option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // SchedulerHints provides a set of hints to the scheduler. + SchedulerHints CreateOptsBuilder +} + +// ToServerCreateMap adds the SchedulerHints option to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + schedulerHints, err := opts.SchedulerHints.ToServerSchedulerHintsCreateMap() + if err != nil { + return nil, err + } + + if len(schedulerHints) == 0 { + return base, nil + } + + base["os:scheduler_hints"] = schedulerHints + + return base, nil +} diff --git a/openstack/compute/v2/extensions/schedulerhints/testing/doc.go b/openstack/compute/v2/extensions/schedulerhints/testing/doc.go new file mode 100644 index 000000000..1915aef2f --- /dev/null +++ b/openstack/compute/v2/extensions/schedulerhints/testing/doc.go @@ -0,0 +1,2 @@ +// schedulerhints unit tests +package testing diff --git a/openstack/compute/v2/extensions/schedulerhints/testing/requests_test.go b/openstack/compute/v2/extensions/schedulerhints/testing/requests_test.go new file mode 100644 index 000000000..9dad39723 --- /dev/null +++ b/openstack/compute/v2/extensions/schedulerhints/testing/requests_test.go @@ -0,0 +1,131 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/schedulerhints" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + th "github.com/huaweicloud/golangsdk/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + schedulerHints := schedulerhints.SchedulerHints{ + Group: "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + DifferentHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + SameHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + Query: []interface{}{">=", "$free_ram_mb", "1024"}, + TargetCell: "foobar", + BuildNearHostIP: "192.168.1.1/24", + AdditionalProperties: map[string]interface{}{"reservation": "a0cf03a5-d921-4877-bb5c-86d26cf818e1"}, + } + + ext := schedulerhints.CreateOptsExt{ + CreateOptsBuilder: base, + SchedulerHints: schedulerHints, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1" + }, + "os:scheduler_hints": { + "group": "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + "different_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "same_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "query": [ + ">=", "$free_ram_mb", "1024" + ], + "target_cell": "foobar", + "build_near_host_ip": "192.168.1.1", + "cidr": "/24", + "reservation": "a0cf03a5-d921-4877-bb5c-86d26cf818e1" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestCreateOptsWithComplexQuery(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + schedulerHints := schedulerhints.SchedulerHints{ + Group: "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + DifferentHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + SameHost: []string{ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287", + }, + Query: []interface{}{"and", []string{">=", "$free_ram_mb", "1024"}, []string{">=", "$free_disk_mb", "204800"}}, + TargetCell: "foobar", + BuildNearHostIP: "192.168.1.1/24", + AdditionalProperties: map[string]interface{}{"reservation": "a0cf03a5-d921-4877-bb5c-86d26cf818e1"}, + } + + ext := schedulerhints.CreateOptsExt{ + CreateOptsBuilder: base, + SchedulerHints: schedulerHints, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1" + }, + "os:scheduler_hints": { + "group": "101aed42-22d9-4a3e-9ba1-21103b0d1aba", + "different_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "same_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ], + "query": [ + "and", + [">=", "$free_ram_mb", "1024"], + [">=", "$free_disk_mb", "204800"] + ], + "target_cell": "foobar", + "build_near_host_ip": "192.168.1.1", + "cidr": "/24", + "reservation": "a0cf03a5-d921-4877-bb5c-86d26cf818e1" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} diff --git a/openstack/compute/v2/extensions/secgroups/doc.go b/openstack/compute/v2/extensions/secgroups/doc.go new file mode 100644 index 000000000..8d3ebf2e5 --- /dev/null +++ b/openstack/compute/v2/extensions/secgroups/doc.go @@ -0,0 +1,112 @@ +/* +Package secgroups provides the ability to manage security groups through the +Nova API. + +This API has been deprecated and will be removed from a future release of the +Nova API service. + +For environments that support this extension, this package can be used +regardless of if either Neutron or nova-network is used as the cloud's network +service. + +Example to List Security Groups + + allPages, err := secroups.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + allSecurityGroups, err := secgroups.ExtractSecurityGroups(allPages) + if err != nil { + panic(err) + } + + for _, sg := range allSecurityGroups { + fmt.Printf("%+v\n", sg) + } + +Example to List Security Groups by Server + + serverID := "aab3ad01-9956-4623-a29b-24afc89a7d36" + + allPages, err := secroups.ListByServer(computeClient, serverID).AllPages() + if err != nil { + panic(err) + } + + allSecurityGroups, err := secgroups.ExtractSecurityGroups(allPages) + if err != nil { + panic(err) + } + + for _, sg := range allSecurityGroups { + fmt.Printf("%+v\n", sg) + } + +Example to Create a Security Group + + createOpts := secgroups.CreateOpts{ + Name: "group_name", + Description: "A Security Group", + } + + sg, err := secgroups.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Security Group Rule + + sgID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + + createOpts := secgroups.CreateRuleOpts{ + ParentGroupID: sgID, + FromPort: 22, + ToPort: 22, + IPProtocol: "tcp", + CIDR: "0.0.0.0/0", + } + + rule, err := secgroups.CreateRule(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Add a Security Group to a Server + + serverID := "aab3ad01-9956-4623-a29b-24afc89a7d36" + sgID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + + err := secgroups.AddServer(computeClient, serverID, sgID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Remove a Security Group from a Server + + serverID := "aab3ad01-9956-4623-a29b-24afc89a7d36" + sgID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + + err := secgroups.RemoveServer(computeClient, serverID, sgID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Delete a Security Group + + + sgID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + err := secgroups.Delete(computeClient, sgID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Delete a Security Group Rule + + ruleID := "6221fe3e-383d-46c9-a3a6-845e66c1e8b4" + err := secgroups.DeleteRule(computeClient, ruleID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package secgroups diff --git a/openstack/compute/v2/extensions/secgroups/requests.go b/openstack/compute/v2/extensions/secgroups/requests.go new file mode 100644 index 000000000..8e3512130 --- /dev/null +++ b/openstack/compute/v2/extensions/secgroups/requests.go @@ -0,0 +1,183 @@ +package secgroups + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +func commonList(client *golangsdk.ServiceClient, url string) pagination.Pager { + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SecurityGroupPage{pagination.SinglePageBase(r)} + }) +} + +// List will return a collection of all the security groups for a particular +// tenant. +func List(client *golangsdk.ServiceClient) pagination.Pager { + return commonList(client, rootURL(client)) +} + +// ListByServer will return a collection of all the security groups which are +// associated with a particular server. +func ListByServer(client *golangsdk.ServiceClient, serverID string) pagination.Pager { + return commonList(client, listByServerURL(client, serverID)) +} + +// GroupOpts is the underlying struct responsible for creating or updating +// security groups. It therefore represents the mutable attributes of a +// security group. +type GroupOpts struct { + // the name of your security group. + Name string `json:"name" required:"true"` + // the description of your security group. + Description string `json:"description" required:"true"` +} + +// CreateOpts is the struct responsible for creating a security group. +type CreateOpts GroupOpts + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecGroupCreateMap() (map[string]interface{}, error) +} + +// ToSecGroupCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "security_group") +} + +// Create will create a new security group. +func Create(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecGroupCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(rootURL(client), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// UpdateOpts is the struct responsible for updating an existing security group. +type UpdateOpts GroupOpts + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSecGroupUpdateMap() (map[string]interface{}, error) +} + +// ToSecGroupUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "security_group") +} + +// Update will modify the mutable properties of a security group, notably its +// name and description. +func Update(client *golangsdk.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSecGroupUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(resourceURL(client, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get will return details for a particular security group. +func Get(client *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(resourceURL(client, id), &r.Body, nil) + return +} + +// Delete will permanently delete a security group from the project. +func Delete(client *golangsdk.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(resourceURL(client, id), nil) + return +} + +// CreateRuleOpts represents the configuration for adding a new rule to an +// existing security group. +type CreateRuleOpts struct { + // ID is the ID of the group that this rule will be added to. + ParentGroupID string `json:"parent_group_id" required:"true"` + + // FromPort is the lower bound of the port range that will be opened. + // Use -1 to allow all ICMP traffic. + FromPort int `json:"from_port"` + + // ToPort is the upper bound of the port range that will be opened. + // Use -1 to allow all ICMP traffic. + ToPort int `json:"to_port"` + + // IPProtocol the protocol type that will be allowed, e.g. TCP. + IPProtocol string `json:"ip_protocol" required:"true"` + + // CIDR is the network CIDR to allow traffic from. + // This is ONLY required if FromGroupID is blank. This represents the IP + // range that will be the source of network traffic to your security group. + // Use 0.0.0.0/0 to allow all IP addresses. + CIDR string `json:"cidr,omitempty" or:"FromGroupID"` + + // FromGroupID represents another security group to allow access. + // This is ONLY required if CIDR is blank. This value represents the ID of a + // group that forwards traffic to the parent group. So, instead of accepting + // network traffic from an entire IP range, you can instead refine the + // inbound source by an existing security group. + FromGroupID string `json:"group_id,omitempty" or:"CIDR"` +} + +// CreateRuleOptsBuilder allows extensions to add additional parameters to the +// CreateRule request. +type CreateRuleOptsBuilder interface { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// ToRuleCreateMap builds a request body from CreateRuleOpts. +func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "security_group_rule") +} + +// CreateRule will add a new rule to an existing security group (whose ID is +// specified in CreateRuleOpts). You have the option of controlling inbound +// traffic from either an IP range (CIDR) or from another security group. +func CreateRule(client *golangsdk.ServiceClient, opts CreateRuleOptsBuilder) (r CreateRuleResult) { + b, err := opts.ToRuleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(rootRuleURL(client), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// DeleteRule will permanently delete a rule from a security group. +func DeleteRule(client *golangsdk.ServiceClient, id string) (r DeleteRuleResult) { + _, r.Err = client.Delete(resourceRuleURL(client, id), nil) + return +} + +func actionMap(prefix, groupName string) map[string]map[string]string { + return map[string]map[string]string{ + prefix + "SecurityGroup": {"name": groupName}, + } +} + +// AddServer will associate a server and a security group, enforcing the +// rules of the group on the server. +func AddServer(client *golangsdk.ServiceClient, serverID, groupName string) (r AddServerResult) { + _, r.Err = client.Post(serverActionURL(client, serverID), actionMap("add", groupName), nil, nil) + return +} + +// RemoveServer will disassociate a server from a security group. +func RemoveServer(client *golangsdk.ServiceClient, serverID, groupName string) (r RemoveServerResult) { + _, r.Err = client.Post(serverActionURL(client, serverID), actionMap("remove", groupName), nil, nil) + return +} diff --git a/openstack/compute/v2/extensions/secgroups/results.go b/openstack/compute/v2/extensions/secgroups/results.go new file mode 100644 index 000000000..3512e65ac --- /dev/null +++ b/openstack/compute/v2/extensions/secgroups/results.go @@ -0,0 +1,214 @@ +package secgroups + +import ( + "encoding/json" + "strconv" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// SecurityGroup represents a security group. +type SecurityGroup struct { + // The unique ID of the group. If Neutron is installed, this ID will be + // represented as a string UUID; if Neutron is not installed, it will be a + // numeric ID. For the sake of consistency, we always cast it to a string. + ID string `json:"-"` + + // The human-readable name of the group, which needs to be unique. + Name string `json:"name"` + + // The human-readable description of the group. + Description string `json:"description"` + + // The rules which determine how this security group operates. + Rules []Rule `json:"rules"` + + // The ID of the tenant to which this security group belongs. + TenantID string `json:"tenant_id"` +} + +func (r *SecurityGroup) UnmarshalJSON(b []byte) error { + type tmp SecurityGroup + var s struct { + tmp + ID interface{} `json:"id"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = SecurityGroup(s.tmp) + + switch t := s.ID.(type) { + case float64: + r.ID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ID = t + } + + return err +} + +// Rule represents a security group rule, a policy which determines how a +// security group operates and what inbound traffic it allows in. +type Rule struct { + // The unique ID. If Neutron is installed, this ID will be + // represented as a string UUID; if Neutron is not installed, it will be a + // numeric ID. For the sake of consistency, we always cast it to a string. + ID string `json:"-"` + + // The lower bound of the port range which this security group should open up. + FromPort int `json:"from_port"` + + // The upper bound of the port range which this security group should open up. + ToPort int `json:"to_port"` + + // The IP protocol (e.g. TCP) which the security group accepts. + IPProtocol string `json:"ip_protocol"` + + // The CIDR IP range whose traffic can be received. + IPRange IPRange `json:"ip_range"` + + // The security group ID to which this rule belongs. + ParentGroupID string `json:"parent_group_id"` + + // Not documented. + Group Group +} + +func (r *Rule) UnmarshalJSON(b []byte) error { + type tmp Rule + var s struct { + tmp + ID interface{} `json:"id"` + ParentGroupID interface{} `json:"parent_group_id"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Rule(s.tmp) + + switch t := s.ID.(type) { + case float64: + r.ID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ID = t + } + + switch t := s.ParentGroupID.(type) { + case float64: + r.ParentGroupID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ParentGroupID = t + } + + return err +} + +// IPRange represents the IP range whose traffic will be accepted by the +// security group. +type IPRange struct { + CIDR string +} + +// Group represents a group. +type Group struct { + TenantID string `json:"tenant_id"` + Name string +} + +// SecurityGroupPage is a single page of a SecurityGroup collection. +type SecurityGroupPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of Security Groups contains any +// results. +func (page SecurityGroupPage) IsEmpty() (bool, error) { + users, err := ExtractSecurityGroups(page) + return len(users) == 0, err +} + +// ExtractSecurityGroups returns a slice of SecurityGroups contained in a +// single page of results. +func ExtractSecurityGroups(r pagination.Page) ([]SecurityGroup, error) { + var s struct { + SecurityGroups []SecurityGroup `json:"security_groups"` + } + err := (r.(SecurityGroupPage)).ExtractInto(&s) + return s.SecurityGroups, err +} + +type commonResult struct { + golangsdk.Result +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret the result as a SecurityGroup. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret the result as a SecurityGroup. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret the result as a SecurityGroup. +type UpdateResult struct { + commonResult +} + +// Extract will extract a SecurityGroup struct from most responses. +func (r commonResult) Extract() (*SecurityGroup, error) { + var s struct { + SecurityGroup *SecurityGroup `json:"security_group"` + } + err := r.ExtractInto(&s) + return s.SecurityGroup, err +} + +// CreateRuleResult represents the result when adding rules to a security group. +// Call its Extract method to interpret the result as a Rule. +type CreateRuleResult struct { + golangsdk.Result +} + +// Extract will extract a Rule struct from a CreateRuleResult. +func (r CreateRuleResult) Extract() (*Rule, error) { + var s struct { + Rule *Rule `json:"security_group_rule"` + } + err := r.ExtractInto(&s) + return s.Rule, err +} + +// DeleteResult is the response from delete operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} + +// DeleteRuleResult is the response from a DeleteRule operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteRuleResult struct { + golangsdk.ErrResult +} + +// AddServerResult is the response from an AddServer operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type AddServerResult struct { + golangsdk.ErrResult +} + +// RemoveServerResult is the response from a RemoveServer operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type RemoveServerResult struct { + golangsdk.ErrResult +} diff --git a/openstack/compute/v2/extensions/secgroups/testing/doc.go b/openstack/compute/v2/extensions/secgroups/testing/doc.go new file mode 100644 index 000000000..c5e60ea09 --- /dev/null +++ b/openstack/compute/v2/extensions/secgroups/testing/doc.go @@ -0,0 +1,2 @@ +// secgroups unit tests +package testing diff --git a/openstack/compute/v2/extensions/secgroups/testing/fixtures.go b/openstack/compute/v2/extensions/secgroups/testing/fixtures.go new file mode 100644 index 000000000..1822a7f4c --- /dev/null +++ b/openstack/compute/v2/extensions/secgroups/testing/fixtures.go @@ -0,0 +1,326 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +const rootPath = "/os-security-groups" + +const listGroupsJSON = ` +{ + "security_groups": [ + { + "description": "default", + "id": "{groupID}", + "name": "default", + "rules": [], + "tenant_id": "openstack" + } + ] +} +` + +func mockListGroupsResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, listGroupsJSON) + }) +} + +func mockListGroupsByServerResponse(t *testing.T, serverID string) { + url := fmt.Sprintf("/servers/%s%s", serverID, rootPath) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, listGroupsJSON) + }) +} + +func mockCreateGroupResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group": { + "name": "test", + "description": "something" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "something", + "id": "{groupID}", + "name": "test", + "rules": [], + "tenant_id": "openstack" + } +} +`) + }) +} + +func mockUpdateGroupResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group": { + "name": "new_name", + "description": "new_desc" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "something", + "id": "{groupID}", + "name": "new_name", + "rules": [], + "tenant_id": "openstack" + } +} +`) + }) +} + +func mockGetGroupsResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "default", + "id": "{groupID}", + "name": "default", + "rules": [ + { + "from_port": 80, + "group": { + "tenant_id": "openstack", + "name": "default" + }, + "ip_protocol": "TCP", + "to_port": 85, + "parent_group_id": "{groupID}", + "ip_range": { + "cidr": "0.0.0.0" + }, + "id": "{ruleID}" + } + ], + "tenant_id": "openstack" + } +} + `) + }) +} + +func mockGetNumericIDGroupResponse(t *testing.T, groupID int) { + url := fmt.Sprintf("%s/%d", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "id": %d + } +} + `, groupID) + }) +} + +func mockGetNumericIDGroupRuleResponse(t *testing.T, groupID int) { + url := fmt.Sprintf("%s/%d", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "id": %d, + "rules": [ + { + "parent_group_id": %d, + "id": %d + } + ] + } +} + `, groupID, groupID, groupID) + }) +} + +func mockDeleteGroupResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockAddRuleResponse(t *testing.T) { + th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group_rule": { + "from_port": 22, + "ip_protocol": "TCP", + "to_port": 22, + "parent_group_id": "{groupID}", + "cidr": "0.0.0.0/0" + } +} `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_rule": { + "from_port": 22, + "group": {}, + "ip_protocol": "TCP", + "to_port": 22, + "parent_group_id": "{groupID}", + "ip_range": { + "cidr": "0.0.0.0/0" + }, + "id": "{ruleID}" + } +}`) + }) +} + +func mockAddRuleResponseICMPZero(t *testing.T) { + th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group_rule": { + "from_port": 0, + "ip_protocol": "ICMP", + "to_port": 0, + "parent_group_id": "{groupID}", + "cidr": "0.0.0.0/0" + } +} `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_rule": { + "from_port": 0, + "group": {}, + "ip_protocol": "ICMP", + "to_port": 0, + "parent_group_id": "{groupID}", + "ip_range": { + "cidr": "0.0.0.0/0" + }, + "id": "{ruleID}" + } +}`) + }) +} + +func mockDeleteRuleResponse(t *testing.T, ruleID string) { + url := fmt.Sprintf("/os-security-group-rules/%s", ruleID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockAddServerToGroupResponse(t *testing.T, serverID string) { + url := fmt.Sprintf("/servers/%s/action", serverID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "addSecurityGroup": { + "name": "test" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockRemoveServerFromGroupResponse(t *testing.T, serverID string) { + url := fmt.Sprintf("/servers/%s/action", serverID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "removeSecurityGroup": { + "name": "test" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/openstack/compute/v2/extensions/secgroups/testing/requests_test.go b/openstack/compute/v2/extensions/secgroups/testing/requests_test.go new file mode 100644 index 000000000..dcad7936c --- /dev/null +++ b/openstack/compute/v2/extensions/secgroups/testing/requests_test.go @@ -0,0 +1,302 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/secgroups" + "github.com/huaweicloud/golangsdk/pagination" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +const ( + serverID = "{serverID}" + groupID = "{groupID}" + ruleID = "{ruleID}" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListGroupsResponse(t) + + count := 0 + + err := secgroups.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := secgroups.ExtractSecurityGroups(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []secgroups.SecurityGroup{ + { + ID: groupID, + Description: "default", + Name: "default", + Rules: []secgroups.Rule{}, + TenantID: "openstack", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestListByServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListGroupsByServerResponse(t, serverID) + + count := 0 + + err := secgroups.ListByServer(client.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := secgroups.ExtractSecurityGroups(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []secgroups.SecurityGroup{ + { + ID: groupID, + Description: "default", + Name: "default", + Rules: []secgroups.Rule{}, + TenantID: "openstack", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateGroupResponse(t) + + opts := secgroups.CreateOpts{ + Name: "test", + Description: "something", + } + + group, err := secgroups.Create(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.SecurityGroup{ + ID: groupID, + Name: "test", + Description: "something", + TenantID: "openstack", + Rules: []secgroups.Rule{}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateGroupResponse(t, groupID) + + opts := secgroups.UpdateOpts{ + Name: "new_name", + Description: "new_desc", + } + + group, err := secgroups.Update(client.ServiceClient(), groupID, opts).Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.SecurityGroup{ + ID: groupID, + Name: "new_name", + Description: "something", + TenantID: "openstack", + Rules: []secgroups.Rule{}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetGroupsResponse(t, groupID) + + group, err := secgroups.Get(client.ServiceClient(), groupID).Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.SecurityGroup{ + ID: groupID, + Description: "default", + Name: "default", + TenantID: "openstack", + Rules: []secgroups.Rule{ + { + FromPort: 80, + ToPort: 85, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "0.0.0.0"}, + Group: secgroups.Group{TenantID: "openstack", Name: "default"}, + ParentGroupID: groupID, + ID: ruleID, + }, + }, + } + + th.AssertDeepEquals(t, expected, group) +} + +func TestGetNumericID(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + numericGroupID := 12345 + + mockGetNumericIDGroupResponse(t, numericGroupID) + + group, err := secgroups.Get(client.ServiceClient(), "12345").Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.SecurityGroup{ID: "12345"} + th.AssertDeepEquals(t, expected, group) +} + +func TestGetNumericRuleID(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + numericGroupID := 12345 + + mockGetNumericIDGroupRuleResponse(t, numericGroupID) + + group, err := secgroups.Get(client.ServiceClient(), "12345").Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.SecurityGroup{ + ID: "12345", + Rules: []secgroups.Rule{ + { + ParentGroupID: "12345", + ID: "12345", + }, + }, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteGroupResponse(t, groupID) + + err := secgroups.Delete(client.ServiceClient(), groupID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAddRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddRuleResponse(t) + + opts := secgroups.CreateRuleOpts{ + ParentGroupID: groupID, + FromPort: 22, + ToPort: 22, + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + rule, err := secgroups.CreateRule(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.Rule{ + FromPort: 22, + ToPort: 22, + Group: secgroups.Group{}, + IPProtocol: "TCP", + ParentGroupID: groupID, + IPRange: secgroups.IPRange{CIDR: "0.0.0.0/0"}, + ID: ruleID, + } + + th.AssertDeepEquals(t, expected, rule) +} + +func TestAddRuleICMPZero(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddRuleResponseICMPZero(t) + + opts := secgroups.CreateRuleOpts{ + ParentGroupID: groupID, + FromPort: 0, + ToPort: 0, + IPProtocol: "ICMP", + CIDR: "0.0.0.0/0", + } + + rule, err := secgroups.CreateRule(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &secgroups.Rule{ + FromPort: 0, + ToPort: 0, + Group: secgroups.Group{}, + IPProtocol: "ICMP", + ParentGroupID: groupID, + IPRange: secgroups.IPRange{CIDR: "0.0.0.0/0"}, + ID: ruleID, + } + + th.AssertDeepEquals(t, expected, rule) +} + +func TestDeleteRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteRuleResponse(t, ruleID) + + err := secgroups.DeleteRule(client.ServiceClient(), ruleID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAddServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddServerToGroupResponse(t, serverID) + + err := secgroups.AddServer(client.ServiceClient(), serverID, "test").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestRemoveServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockRemoveServerFromGroupResponse(t, serverID) + + err := secgroups.RemoveServer(client.ServiceClient(), serverID, "test").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/openstack/compute/v2/extensions/secgroups/urls.go b/openstack/compute/v2/extensions/secgroups/urls.go new file mode 100644 index 000000000..8d54b6ab1 --- /dev/null +++ b/openstack/compute/v2/extensions/secgroups/urls.go @@ -0,0 +1,32 @@ +package secgroups + +import "github.com/huaweicloud/golangsdk" + +const ( + secgrouppath = "os-security-groups" + rulepath = "os-security-group-rules" +) + +func resourceURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL(secgrouppath, id) +} + +func rootURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL(secgrouppath) +} + +func listByServerURL(c *golangsdk.ServiceClient, serverID string) string { + return c.ServiceURL("servers", serverID, secgrouppath) +} + +func rootRuleURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL(rulepath) +} + +func resourceRuleURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL(rulepath, id) +} + +func serverActionURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL("servers", id, "action") +} diff --git a/openstack/compute/v2/extensions/startstop/doc.go b/openstack/compute/v2/extensions/startstop/doc.go new file mode 100644 index 000000000..ab97edb77 --- /dev/null +++ b/openstack/compute/v2/extensions/startstop/doc.go @@ -0,0 +1,19 @@ +/* +Package startstop provides functionality to start and stop servers that have +been provisioned by the OpenStack Compute service. + +Example to Stop and Start a Server + + serverID := "47b6b7b7-568d-40e4-868c-d5c41735532e" + + err := startstop.Stop(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + + err := startstop.Start(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package startstop diff --git a/openstack/compute/v2/extensions/startstop/requests.go b/openstack/compute/v2/extensions/startstop/requests.go new file mode 100644 index 000000000..df4aba08f --- /dev/null +++ b/openstack/compute/v2/extensions/startstop/requests.go @@ -0,0 +1,19 @@ +package startstop + +import "github.com/huaweicloud/golangsdk" + +func actionURL(client *golangsdk.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +// Start is the operation responsible for starting a Compute server. +func Start(client *golangsdk.ServiceClient, id string) (r StartResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-start": nil}, nil, nil) + return +} + +// Stop is the operation responsible for stopping a Compute server. +func Stop(client *golangsdk.ServiceClient, id string) (r StopResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-stop": nil}, nil, nil) + return +} diff --git a/openstack/compute/v2/extensions/startstop/results.go b/openstack/compute/v2/extensions/startstop/results.go new file mode 100644 index 000000000..a1f54f2f4 --- /dev/null +++ b/openstack/compute/v2/extensions/startstop/results.go @@ -0,0 +1,15 @@ +package startstop + +import "github.com/huaweicloud/golangsdk" + +// StartResult is the response from a Start operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StartResult struct { + golangsdk.ErrResult +} + +// StopResult is the response from Stop operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StopResult struct { + golangsdk.ErrResult +} diff --git a/openstack/compute/v2/extensions/startstop/testing/doc.go b/openstack/compute/v2/extensions/startstop/testing/doc.go new file mode 100644 index 000000000..b6c5b8c14 --- /dev/null +++ b/openstack/compute/v2/extensions/startstop/testing/doc.go @@ -0,0 +1,2 @@ +// startstop unit tests +package testing diff --git a/openstack/compute/v2/extensions/startstop/testing/fixtures.go b/openstack/compute/v2/extensions/startstop/testing/fixtures.go new file mode 100644 index 000000000..0e1253f5a --- /dev/null +++ b/openstack/compute/v2/extensions/startstop/testing/fixtures.go @@ -0,0 +1,27 @@ +package testing + +import ( + "net/http" + "testing" + + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func mockStartServerResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{"os-start": null}`) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockStopServerResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{"os-stop": null}`) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/openstack/compute/v2/extensions/startstop/testing/requests_test.go b/openstack/compute/v2/extensions/startstop/testing/requests_test.go new file mode 100644 index 000000000..3ceaaaf7d --- /dev/null +++ b/openstack/compute/v2/extensions/startstop/testing/requests_test.go @@ -0,0 +1,31 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/startstop" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +const serverID = "{serverId}" + +func TestStart(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockStartServerResponse(t, serverID) + + err := startstop.Start(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestStop(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockStopServerResponse(t, serverID) + + err := startstop.Stop(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/openstack/compute/v2/extensions/tags/doc.go b/openstack/compute/v2/extensions/tags/doc.go new file mode 100644 index 000000000..075640c3c --- /dev/null +++ b/openstack/compute/v2/extensions/tags/doc.go @@ -0,0 +1,43 @@ +/* +Package tags provides functionality to operate with server tags that have been +provisioned by the OpenStack Comupte service. + +Examples + + serverID := "47b6b7b7-568d-40e4-868c-d5c41735532e" + + // list all tags + tags, err := ListTags(client, serverID).Extract() + if err != nil { + panic(err) + } + + // put tags + err := PutTags(client, serverID, "tag1", "tag2") + if err != nil { + panic(err) + } + + // clean tags + err := CleanTags(client, serverID) + if err != nil { + panic(err) + } + + // check tag existance + err := CheckTag(client, serverID, "tag1") + + // add a tag to server + err := AddTag(client, serverID, "tagN") + if err != nil { + panic(err) + } + + // delete a tag from server tag list + err := DeleteTag(client, serverID, "tagN") + if err != nil { + panic(err) + } + +*/ +package tags diff --git a/openstack/compute/v2/extensions/tags/requests.go b/openstack/compute/v2/extensions/tags/requests.go new file mode 100644 index 000000000..198ebc842 --- /dev/null +++ b/openstack/compute/v2/extensions/tags/requests.go @@ -0,0 +1,47 @@ +package tags + +import ( + "github.com/huaweicloud/golangsdk" +) + +// ListTags list all tags of server specified by id +func ListTags(client *golangsdk.ServiceClient, id string) (r ListResult) { + _, r.Err = client.Get(listTags(client, id), &r.Body, nil) + return +} + +// PutTags put tags to server specified by id +func PutTags(client *golangsdk.ServiceClient, id string, tags ...string) error { + _, err := client.Put(replaceTags(client, id), map[string][]string{"tags": tags}, nil, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return err +} + +// ClientTags remove all tags of a server +func CleanTags(client *golangsdk.ServiceClient, id string) error { + _, err := client.Delete(deleteTags(client, id), nil) + return err +} + +// CheckTag check if the tag exists on the serve +func CheckTag(client *golangsdk.ServiceClient, id, tag string) error { + _, err := client.Get(checkTag(client, id, tag), nil, &golangsdk.RequestOpts{ + OkCodes: []int{204}, + }) + return err +} + +// AddTag add a tag to a server +func AddTag(client *golangsdk.ServiceClient, id, tag string) error { + _, err := client.Put(addTag(client, id, tag), nil, nil, &golangsdk.RequestOpts{ + OkCodes: []int{204}, + }) + return err +} + +// DeleteTag delete a tag from a server +func DeleteTag(client *golangsdk.ServiceClient, id, tag string) error { + _, err := client.Delete(deleteTag(client, id, tag), nil) + return err +} diff --git a/openstack/compute/v2/extensions/tags/results.go b/openstack/compute/v2/extensions/tags/results.go new file mode 100644 index 000000000..bd6dc674a --- /dev/null +++ b/openstack/compute/v2/extensions/tags/results.go @@ -0,0 +1,22 @@ +package tags + +import ( + "github.com/huaweicloud/golangsdk" +) + +// ListResult is the result list of tags +type ListResult struct { + golangsdk.Result +} + +// Extract extract http response to golang struct +func (r ListResult) Extract() (*ServerTags, error) { + var t ServerTags + err := r.Result.ExtractInto(&t) + return &t, err +} + +// ServerTags represents server tag list +type ServerTags struct { + Tags []string `json:"tags"` +} diff --git a/openstack/compute/v2/extensions/tags/testing/doc.go b/openstack/compute/v2/extensions/tags/testing/doc.go new file mode 100644 index 000000000..fce31e522 --- /dev/null +++ b/openstack/compute/v2/extensions/tags/testing/doc.go @@ -0,0 +1,2 @@ +// tags unit tests +package testing diff --git a/openstack/compute/v2/extensions/tags/testing/fixtures.go b/openstack/compute/v2/extensions/tags/testing/fixtures.go new file mode 100644 index 000000000..376eb45af --- /dev/null +++ b/openstack/compute/v2/extensions/tags/testing/fixtures.go @@ -0,0 +1,60 @@ +package testing + +import ( + "net/http" + "testing" + + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func mockListTagsResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/tags", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"tags":["tag1","tag2"]}`)) + }) +} + +func mockPutTagsResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/tags", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestBody(t, r, `{"tags":["tag1","tag2"]}`) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"tags":["tag1","tag2"]}`)) + }) +} + +func mockCleanTagsResponse(t *testing.T, id string) { + th.Mux.HandleFunc("/servers/"+id+"/tags", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func mockCheckTagResponse(t *testing.T, id, tag string) { + th.Mux.HandleFunc("/servers/"+id+"/tags/"+tag, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func mockAddTagResponse(t *testing.T, id, tag string) { + th.Mux.HandleFunc("/servers/"+id+"/tags/"+tag, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +func mockDeleteTagResponse(t *testing.T, id, tag string) { + th.Mux.HandleFunc("/servers/"+id+"/tags/"+tag, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/openstack/compute/v2/extensions/tags/testing/requests_test.go b/openstack/compute/v2/extensions/tags/testing/requests_test.go new file mode 100644 index 000000000..cd303d07c --- /dev/null +++ b/openstack/compute/v2/extensions/tags/testing/requests_test.go @@ -0,0 +1,65 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/tags" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +const serverID = "{serverId}" + +func TestListTags(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + mockListTagsResponse(t, serverID) + + _, err := tags.ListTags(client.ServiceClient(), serverID).Extract() + th.AssertNoErr(t, err) +} + +func TestPutTags(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + mockPutTagsResponse(t, serverID) + + err := tags.PutTags(client.ServiceClient(), serverID, "tag1", "tag2") + th.AssertNoErr(t, err) +} + +func TestCleanTags(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + mockCleanTagsResponse(t, serverID) + + err := tags.CleanTags(client.ServiceClient(), serverID) + th.AssertNoErr(t, err) +} + +func TestCheckTag(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + mockCheckTagResponse(t, serverID, "tagN") + + err := tags.CheckTag(client.ServiceClient(), serverID, "tagN") + th.AssertNoErr(t, err) +} + +func TestAddTag(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + mockAddTagResponse(t, serverID, "tagN") + + err := tags.AddTag(client.ServiceClient(), serverID, "tagN") + th.AssertNoErr(t, err) +} + +func TestDeleteTag(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + mockDeleteTagResponse(t, serverID, "tagN") + + err := tags.DeleteTag(client.ServiceClient(), serverID, "tagN") + th.AssertNoErr(t, err) +} diff --git a/openstack/compute/v2/extensions/tags/urls.go b/openstack/compute/v2/extensions/tags/urls.go new file mode 100644 index 000000000..a944640b1 --- /dev/null +++ b/openstack/compute/v2/extensions/tags/urls.go @@ -0,0 +1,23 @@ +package tags + +import "github.com/huaweicloud/golangsdk" + +func operateTagList(client *golangsdk.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "tags") +} + +var ( + listTags = operateTagList + replaceTags = operateTagList + deleteTags = operateTagList +) + +func operateTag(client *golangsdk.ServiceClient, id, tag string) string { + return client.ServiceURL("servers", id, "tags", tag) +} + +var ( + checkTag = operateTag + addTag = operateTag + deleteTag = operateTag +) diff --git a/openstack/compute/v2/extensions/tenantnetworks/doc.go b/openstack/compute/v2/extensions/tenantnetworks/doc.go new file mode 100644 index 000000000..a32e8ffd5 --- /dev/null +++ b/openstack/compute/v2/extensions/tenantnetworks/doc.go @@ -0,0 +1,26 @@ +/* +Package tenantnetworks provides the ability for tenants to see information +about the networks they have access to. + +This is a deprecated API and will be removed from the Nova API service in a +future version. + +This API works in both Neutron and nova-network based OpenStack clouds. + +Example to List Networks Available to a Tenant + + allPages, err := tenantnetworks.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + allNetworks, err := tenantnetworks.ExtractNetworks(allPages) + if err != nil { + panic(err) + } + + for _, network := range allNetworks { + fmt.Printf("%+v\n", network) + } +*/ +package tenantnetworks diff --git a/openstack/compute/v2/extensions/tenantnetworks/requests.go b/openstack/compute/v2/extensions/tenantnetworks/requests.go new file mode 100644 index 000000000..8a5c3600e --- /dev/null +++ b/openstack/compute/v2/extensions/tenantnetworks/requests.go @@ -0,0 +1,19 @@ +package tenantnetworks + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of Networks. +func List(client *golangsdk.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.SinglePageBase(r)} + }) +} + +// Get returns data about a previously created Network. +func Get(client *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} diff --git a/openstack/compute/v2/extensions/tenantnetworks/results.go b/openstack/compute/v2/extensions/tenantnetworks/results.go new file mode 100644 index 000000000..1448a59cb --- /dev/null +++ b/openstack/compute/v2/extensions/tenantnetworks/results.go @@ -0,0 +1,58 @@ +package tenantnetworks + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// A Network represents a network that a server communicates on. +type Network struct { + // CIDR is the IPv4 subnet. + CIDR string `json:"cidr"` + + // ID is the UUID of the network. + ID string `json:"id"` + + // Name is the common name that the network has. + Name string `json:"label"` +} + +// NetworkPage stores a single page of all Networks results from a List call. +type NetworkPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a NetworkPage is empty. +func (page NetworkPage) IsEmpty() (bool, error) { + va, err := ExtractNetworks(page) + return len(va) == 0, err +} + +// ExtractNetworks interprets a page of results as a slice of Network. +func ExtractNetworks(r pagination.Page) ([]Network, error) { + var s struct { + Networks []Network `json:"networks"` + } + err := (r.(NetworkPage)).ExtractInto(&s) + return s.Networks, err +} + +type NetworkResult struct { + golangsdk.Result +} + +// Extract is a method that attempts to interpret any Network resource response +// as a Network struct. +func (r NetworkResult) Extract() (*Network, error) { + var s struct { + Network *Network `json:"network"` + } + err := r.ExtractInto(&s) + return s.Network, err +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a Network. +type GetResult struct { + NetworkResult +} diff --git a/openstack/compute/v2/extensions/tenantnetworks/testing/doc.go b/openstack/compute/v2/extensions/tenantnetworks/testing/doc.go new file mode 100644 index 000000000..4639153ff --- /dev/null +++ b/openstack/compute/v2/extensions/tenantnetworks/testing/doc.go @@ -0,0 +1,2 @@ +// tenantnetworks unit tests +package testing diff --git a/openstack/compute/v2/extensions/tenantnetworks/testing/fixtures.go b/openstack/compute/v2/extensions/tenantnetworks/testing/fixtures.go new file mode 100644 index 000000000..fd69c580c --- /dev/null +++ b/openstack/compute/v2/extensions/tenantnetworks/testing/fixtures.go @@ -0,0 +1,83 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/tenantnetworks" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "networks": [ + { + "cidr": "10.0.0.0/29", + "id": "20c8acc0-f747-4d71-a389-46d078ebf047", + "label": "mynet_0" + }, + { + "cidr": "10.0.0.10/29", + "id": "20c8acc0-f747-4d71-a389-46d078ebf000", + "label": "mynet_1" + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "network": { + "cidr": "10.0.0.10/29", + "id": "20c8acc0-f747-4d71-a389-46d078ebf000", + "label": "mynet_1" + } +} +` + +// FirstNetwork is the first result in ListOutput. +var nilTime time.Time +var FirstNetwork = tenantnetworks.Network{ + CIDR: "10.0.0.0/29", + ID: "20c8acc0-f747-4d71-a389-46d078ebf047", + Name: "mynet_0", +} + +// SecondNetwork is the second result in ListOutput. +var SecondNetwork = tenantnetworks.Network{ + CIDR: "10.0.0.10/29", + ID: "20c8acc0-f747-4d71-a389-46d078ebf000", + Name: "mynet_1", +} + +// ExpectedNetworkSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedNetworkSlice = []tenantnetworks.Network{FirstNetwork, SecondNetwork} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-tenant-networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing network. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-tenant-networks/20c8acc0-f747-4d71-a389-46d078ebf000", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} diff --git a/openstack/compute/v2/extensions/tenantnetworks/testing/requests_test.go b/openstack/compute/v2/extensions/tenantnetworks/testing/requests_test.go new file mode 100644 index 000000000..77c98c7c5 --- /dev/null +++ b/openstack/compute/v2/extensions/tenantnetworks/testing/requests_test.go @@ -0,0 +1,38 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/tenantnetworks" + "github.com/huaweicloud/golangsdk/pagination" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t) + + count := 0 + err := tenantnetworks.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := tenantnetworks.ExtractNetworks(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedNetworkSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := tenantnetworks.Get(client.ServiceClient(), "20c8acc0-f747-4d71-a389-46d078ebf000").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondNetwork, actual) +} diff --git a/openstack/compute/v2/extensions/tenantnetworks/urls.go b/openstack/compute/v2/extensions/tenantnetworks/urls.go new file mode 100644 index 000000000..1917b1703 --- /dev/null +++ b/openstack/compute/v2/extensions/tenantnetworks/urls.go @@ -0,0 +1,17 @@ +package tenantnetworks + +import "github.com/huaweicloud/golangsdk" + +const resourcePath = "os-tenant-networks" + +func resourceURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *golangsdk.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/openstack/compute/v2/extensions/volumeattach/doc.go b/openstack/compute/v2/extensions/volumeattach/doc.go new file mode 100644 index 000000000..484eb2000 --- /dev/null +++ b/openstack/compute/v2/extensions/volumeattach/doc.go @@ -0,0 +1,30 @@ +/* +Package volumeattach provides the ability to attach and detach volumes +from servers. + +Example to Attach a Volume + + serverID := "7ac8686c-de71-4acb-9600-ec18b1a1ed6d" + volumeID := "87463836-f0e2-4029-abf6-20c8892a3103" + + createOpts := volumeattach.CreateOpts{ + Device: "/dev/vdc", + VolumeID: volumeID, + } + + result, err := volumeattach.Create(computeClient, serverID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Detach a Volume + + serverID := "7ac8686c-de71-4acb-9600-ec18b1a1ed6d" + attachmentID := "ed081613-1c9b-4231-aa5e-ebfd4d87f983" + + err := volumeattach.Delete(computeClient, serverID, attachmentID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package volumeattach diff --git a/openstack/compute/v2/extensions/volumeattach/requests.go b/openstack/compute/v2/extensions/volumeattach/requests.go new file mode 100644 index 000000000..d6cdc54fd --- /dev/null +++ b/openstack/compute/v2/extensions/volumeattach/requests.go @@ -0,0 +1,60 @@ +package volumeattach + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of +// VolumeAttachments. +func List(client *golangsdk.ServiceClient, serverID string) pagination.Pager { + return pagination.NewPager(client, listURL(client, serverID), func(r pagination.PageResult) pagination.Page { + return VolumeAttachmentPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + ToVolumeAttachmentCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies volume attachment creation or import parameters. +type CreateOpts struct { + // Device is the device that the volume will attach to the instance as. + // Omit for "auto". + Device string `json:"device,omitempty"` + + // VolumeID is the ID of the volume to attach to the instance. + VolumeID string `json:"volumeId" required:"true"` +} + +// ToVolumeAttachmentCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToVolumeAttachmentCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "volumeAttachment") +} + +// Create requests the creation of a new volume attachment on the server. +func Create(client *golangsdk.ServiceClient, serverID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeAttachmentCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client, serverID), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns public data about a previously created VolumeAttachment. +func Get(client *golangsdk.ServiceClient, serverID, attachmentID string) (r GetResult) { + _, r.Err = client.Get(getURL(client, serverID, attachmentID), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous stored VolumeAttachment from +// the server. +func Delete(client *golangsdk.ServiceClient, serverID, attachmentID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, serverID, attachmentID), nil) + return +} diff --git a/openstack/compute/v2/extensions/volumeattach/results.go b/openstack/compute/v2/extensions/volumeattach/results.go new file mode 100644 index 000000000..92bc8814d --- /dev/null +++ b/openstack/compute/v2/extensions/volumeattach/results.go @@ -0,0 +1,77 @@ +package volumeattach + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// VolumeAttachment contains attachment information between a volume +// and server. +type VolumeAttachment struct { + // ID is a unique id of the attachment. + ID string `json:"id"` + + // Device is what device the volume is attached as. + Device string `json:"device"` + + // VolumeID is the ID of the attached volume. + VolumeID string `json:"volumeId"` + + // ServerID is the ID of the instance that has the volume attached. + ServerID string `json:"serverId"` +} + +// VolumeAttachmentPage stores a single page all of VolumeAttachment +// results from a List call. +type VolumeAttachmentPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a VolumeAttachmentPage is empty. +func (page VolumeAttachmentPage) IsEmpty() (bool, error) { + va, err := ExtractVolumeAttachments(page) + return len(va) == 0, err +} + +// ExtractVolumeAttachments interprets a page of results as a slice of +// VolumeAttachment. +func ExtractVolumeAttachments(r pagination.Page) ([]VolumeAttachment, error) { + var s struct { + VolumeAttachments []VolumeAttachment `json:"volumeAttachments"` + } + err := (r.(VolumeAttachmentPage)).ExtractInto(&s) + return s.VolumeAttachments, err +} + +// VolumeAttachmentResult is the result from a volume attachment operation. +type VolumeAttachmentResult struct { + golangsdk.Result +} + +// Extract is a method that attempts to interpret any VolumeAttachment resource +// response as a VolumeAttachment struct. +func (r VolumeAttachmentResult) Extract() (*VolumeAttachment, error) { + var s struct { + VolumeAttachment *VolumeAttachment `json:"volumeAttachment"` + } + err := r.ExtractInto(&s) + return s.VolumeAttachment, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a VolumeAttachment. +type CreateResult struct { + VolumeAttachmentResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a VolumeAttachment. +type GetResult struct { + VolumeAttachmentResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} diff --git a/openstack/compute/v2/extensions/volumeattach/testing/doc.go b/openstack/compute/v2/extensions/volumeattach/testing/doc.go new file mode 100644 index 000000000..11dfc0694 --- /dev/null +++ b/openstack/compute/v2/extensions/volumeattach/testing/doc.go @@ -0,0 +1,2 @@ +// volumeattach unit tests +package testing diff --git a/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go b/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go new file mode 100644 index 000000000..b4537c8b0 --- /dev/null +++ b/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go @@ -0,0 +1,108 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "volumeAttachments": [ + { + "device": "/dev/vdd", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f803", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803" + }, + { + "device": "/dev/vdc", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804" + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "volumeAttachment": { + "device": "/dev/vdc", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804" + } +} +` + +// CreateOutput is a sample response to a Create call. +const CreateOutput = ` +{ + "volumeAttachment": { + "device": "/dev/vdc", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804" + } +} +` + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing attachment +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments/a26887c6-c47b-4654-abb5-dfadf7d3f804", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request +// for a new attachment +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "volumeAttachment": { + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "device": "/dev/vdc" + } +} +`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// an existing attachment +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments/a26887c6-c47b-4654-abb5-dfadf7d3f804", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/openstack/compute/v2/extensions/volumeattach/testing/requests_test.go b/openstack/compute/v2/extensions/volumeattach/testing/requests_test.go new file mode 100644 index 000000000..972cfb2ee --- /dev/null +++ b/openstack/compute/v2/extensions/volumeattach/testing/requests_test.go @@ -0,0 +1,102 @@ +package testing + +import ( + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/volumeattach" + "github.com/huaweicloud/golangsdk/pagination" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// FirstVolumeAttachment is the first result in ListOutput. +var FirstVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdd", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", +} + +// SecondVolumeAttachment is the first result in ListOutput. +var SecondVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdc", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", +} + +// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedVolumeAttachmentSlice = []volumeattach.VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment} + +//CreatedVolumeAttachment is the parsed result from CreatedOutput. +var CreatedVolumeAttachment = volumeattach.VolumeAttachment{ + Device: "/dev/vdc", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleListSuccessfully(t) + + serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + count := 0 + err := volumeattach.List(client.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := volumeattach.ExtractVolumeAttachments(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedVolumeAttachmentSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleCreateSuccessfully(t) + + serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + actual, err := volumeattach.Create(client.ServiceClient(), serverID, volumeattach.CreateOpts{ + Device: "/dev/vdc", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedVolumeAttachment, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetSuccessfully(t) + + aID := "a26887c6-c47b-4654-abb5-dfadf7d3f804" + serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + actual, err := volumeattach.Get(client.ServiceClient(), serverID, aID).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondVolumeAttachment, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleDeleteSuccessfully(t) + + aID := "a26887c6-c47b-4654-abb5-dfadf7d3f804" + serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + err := volumeattach.Delete(client.ServiceClient(), serverID, aID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/openstack/compute/v2/extensions/volumeattach/urls.go b/openstack/compute/v2/extensions/volumeattach/urls.go new file mode 100644 index 000000000..7ed54eafa --- /dev/null +++ b/openstack/compute/v2/extensions/volumeattach/urls.go @@ -0,0 +1,25 @@ +package volumeattach + +import "github.com/huaweicloud/golangsdk" + +const resourcePath = "os-volume_attachments" + +func resourceURL(c *golangsdk.ServiceClient, serverID string) string { + return c.ServiceURL("servers", serverID, resourcePath) +} + +func listURL(c *golangsdk.ServiceClient, serverID string) string { + return resourceURL(c, serverID) +} + +func createURL(c *golangsdk.ServiceClient, serverID string) string { + return resourceURL(c, serverID) +} + +func getURL(c *golangsdk.ServiceClient, serverID, aID string) string { + return c.ServiceURL("servers", serverID, resourcePath, aID) +} + +func deleteURL(c *golangsdk.ServiceClient, serverID, aID string) string { + return getURL(c, serverID, aID) +} diff --git a/openstack/compute/v2/flavors/doc.go b/openstack/compute/v2/flavors/doc.go new file mode 100644 index 000000000..18b5b6f84 --- /dev/null +++ b/openstack/compute/v2/flavors/doc.go @@ -0,0 +1,137 @@ +/* +Package flavors provides information and interaction with the flavor API +in the OpenStack Compute service. + +A flavor is an available hardware configuration for a server. Each flavor +has a unique combination of disk space, memory capacity and priority for CPU +time. + +Example to List Flavors + + listOpts := flavors.ListOpts{ + AccessType: flavors.PublicAccess, + } + + allPages, err := flavors.ListDetail(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + panic(err) + } + + for _, flavor := range allFlavors { + fmt.Printf("%+v\n", flavor) + } + +Example to Create a Flavor + + createOpts := flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: golangsdk.IntToPointer(1), + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + + flavor, err := flavors.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to List Flavor Access + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + allPages, err := flavors.ListAccesses(computeClient, flavorID).AllPages() + if err != nil { + panic(err) + } + + allAccesses, err := flavors.ExtractAccesses(allPages) + if err != nil { + panic(err) + } + + for _, access := range allAccesses { + fmt.Printf("%+v", access) + } + +Example to Grant Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.AddAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.AddAccess(computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove/Revoke Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.RemoveAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.RemoveAccess(computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + createOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_policy": "CPU-POLICY", + "hw:cpu_thread_policy": "CPU-THREAD-POLICY", + } + createdExtraSpecs, err := flavors.CreateExtraSpecs(computeClient, flavorID, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", createdExtraSpecs) + +Example to Get Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + extraSpecs, err := flavors.ListExtraSpecs(computeClient, flavorID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", extraSpecs) + +Example to Update Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + updateOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_thread_policy": "CPU-THREAD-POLICY-UPDATED", + } + updatedExtraSpec, err := flavors.UpdateExtraSpec(computeClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", updatedExtraSpec) + +Example to Delete an Extra Spec for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + err := flavors.DeleteExtraSpec(computeClient, flavorID, "hw:cpu_thread_policy").ExtractErr() + if err != nil { + panic(err) + } +*/ +package flavors diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go new file mode 100644 index 000000000..6ee688696 --- /dev/null +++ b/openstack/compute/v2/flavors/requests.go @@ -0,0 +1,150 @@ +package flavors + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// DetailOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListDetailOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +/* + AccessType maps to OpenStack's Flavor.is_public field. Although the is_public + field is boolean, the request options are ternary, which is why AccessType is + a string. The following values are allowed: + + The AccessType arguement is optional, and if it is not supplied, OpenStack + returns the PublicAccess flavors. +*/ +type AccessType string + +const ( + // PublicAccess returns public flavors and private flavors associated with + // that project. + PublicAccess AccessType = "true" + + // PrivateAccess (admin only) returns private flavors, across all projects. + PrivateAccess AccessType = "false" + + // AllAccess (admin only) returns public and private flavors across all + // projects. + AllAccess AccessType = "None" +) + +/* + ListOpts filters the results returned by the List() function. + For example, a flavor with a minDisk field of 10 will not be returned if you + specify MinDisk set to 20. + + Typically, software will use the last ID of the previous call to List to set + the Marker for the current call. +*/ +type ListDetailOpts struct { + // MinDisk and MinRAM, if provided, elides flavors which do not meet your + // criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // Limit instructs List to refrain from sending excessively large lists of + // flavors. + Limit int `q:"limit"` + + // AccessType, if provided, instructs List which set of flavors to return. + // If IsPublic not provided, flavors for the current project are returned. + AccessType AccessType `q:"is_public"` + + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListDetailOpts) ToFlavorListQuery() (string, error) { + q, err := golangsdk.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail instructs OpenStack to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier +// processing. +func Detail(client *golangsdk.ServiceClient, opts ListDetailOptsBuilder) pagination.Pager { + url := detailURL(client) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +func List(client *golangsdk.ServiceClient, opts ListDetailOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details of a single flavor. Use ExtractFlavor to convert its +// result into a Flavor. +func Get(client *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ExtraSpecs requests all the extra-specs for the given flavor ID. +func ListExtraSpecs(client *golangsdk.ServiceClient, flavorID string) (r ListExtraSpecsResult) { + _, r.Err = client.Get(extraSpecsListURL(client, flavorID), &r.Body, nil) + return +} + +// IDFromName is a convienience function that returns a flavor's ID given its +// name. +func IDFromName(client *golangsdk.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := Detail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractFlavors(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &golangsdk.ErrResourceNotFound{} + err.ResourceType = "flavor" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &golangsdk.ErrMultipleResourcesFound{} + err.ResourceType = "flavor" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go new file mode 100644 index 000000000..99d9844be --- /dev/null +++ b/openstack/compute/v2/flavors/results.go @@ -0,0 +1,153 @@ +package flavors + +import ( + "encoding/json" + "strconv" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +type commonResult struct { + golangsdk.Result +} + +// GetResult is the response of a Get operations. Call its Extract method to +// interpret it as a Flavor. +type GetResult struct { + commonResult +} + +// Extract provides access to the individual Flavor returned by the Get and +// Create functions. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor represent (virtual) hardware configurations for server resources +// in a region. +type Flavor struct { + // ID is the flavor's unique ID. + ID string `json:"id"` + + // Disk is the amount of root disk, measured in GB. + Disk int `json:"disk"` + + // RAM is the amount of memory, measured in MB. + RAM int `json:"ram"` + + // Name is the name of the flavor. + Name string `json:"name"` + + // RxTxFactor describes bandwidth alterations of the flavor. + RxTxFactor float64 `json:"rxtx_factor"` + + // Swap is the amount of swap space, measured in MB. + Swap int `json:"swap"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `json:"vcpus"` + + // IsPublic indicates whether the flavor is public. + IsPublic bool `json:"os-flavor-access:is_public"` + + // Ephemeral is the amount of ephemeral disk space, measured in GB. + Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"` + + Links []struct { + Href string `json:"href"` + Rel string `json:"rel"` + } `json:"links"` +} + +func (r *Flavor) UnmarshalJSON(b []byte) error { + type tmp Flavor + var s struct { + tmp + Swap interface{} `json:"swap"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Flavor(s.tmp) + + switch t := s.Swap.(type) { + case float64: + r.Swap = int(t) + case string: + switch t { + case "": + r.Swap = 0 + default: + swap, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Swap = int(swap) + } + } + + return nil +} + +// FlavorPage contains a single page of all flavors from a ListDetails call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a FlavorPage contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []golangsdk.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return golangsdk.ExtractNextURL(s.Links) +} + +// ExtractFlavors provides access to the list of flavors in a page acquired +// from the ListDetail operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} + +// Extract interprets any extraSpecsResult as ExtraSpecs, if possible. +func (r extraSpecsResult) Extract() (map[string]string, error) { + var s struct { + ExtraSpecs map[string]string `json:"extra_specs"` + } + err := r.ExtractInto(&s) + return s.ExtraSpecs, err +} + +// extraSpecsResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type extraSpecsResult struct { + golangsdk.Result +} + +// ListExtraSpecsResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type ListExtraSpecsResult struct { + extraSpecsResult +} diff --git a/openstack/compute/v2/flavors/testing/doc.go b/openstack/compute/v2/flavors/testing/doc.go new file mode 100644 index 000000000..c27087b56 --- /dev/null +++ b/openstack/compute/v2/flavors/testing/doc.go @@ -0,0 +1,2 @@ +// flavors unit tests +package testing diff --git a/openstack/compute/v2/flavors/testing/fixtures.go b/openstack/compute/v2/flavors/testing/fixtures.go new file mode 100644 index 000000000..001b7909a --- /dev/null +++ b/openstack/compute/v2/flavors/testing/fixtures.go @@ -0,0 +1,116 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// ExtraSpecsGetBody provides a GET result of the extra_specs for a flavor +const ExtraSpecsGetBody = ` +{ + "extra_specs" : { + "hw:cpu_policy": "CPU-POLICY", + "hw:cpu_thread_policy": "CPU-THREAD-POLICY" + } +} +` + +// GetExtraSpecBody provides a GET result of a particular extra_spec for a flavor +const GetExtraSpecBody = ` +{ + "hw:cpu_policy": "CPU-POLICY" +} +` + +// UpdatedExtraSpecBody provides an PUT result of a particular updated extra_spec for a flavor +const UpdatedExtraSpecBody = ` +{ + "hw:cpu_policy": "CPU-POLICY-2" +} +` + +// ExtraSpecs is the expected extra_specs returned from GET on a flavor's extra_specs +var ExtraSpecs = map[string]string{ + "hw:cpu_policy": "CPU-POLICY", + "hw:cpu_thread_policy": "CPU-THREAD-POLICY", +} + +// ExtraSpec is the expected extra_spec returned from GET on a flavor's extra_specs +var ExtraSpec = map[string]string{ + "hw:cpu_policy": "CPU-POLICY", +} + +// UpdatedExtraSpec is the expected extra_spec returned from PUT on a flavor's extra_specs +var UpdatedExtraSpec = map[string]string{ + "hw:cpu_policy": "CPU-POLICY-2", +} + +func HandleExtraSpecsListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/flavors/1/os-extra_specs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ExtraSpecsGetBody) + }) +} + +func HandleExtraSpecGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/flavors/1/os-extra_specs/hw:cpu_policy", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, GetExtraSpecBody) + }) +} + +func HandleExtraSpecsCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/flavors/1/os-extra_specs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, `{ + "extra_specs": { + "hw:cpu_policy": "CPU-POLICY", + "hw:cpu_thread_policy": "CPU-THREAD-POLICY" + } + }`) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ExtraSpecsGetBody) + }) +} + +func HandleExtraSpecUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/flavors/1/os-extra_specs/hw:cpu_policy", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, `{ + "hw:cpu_policy": "CPU-POLICY-2" + }`) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, UpdatedExtraSpecBody) + }) +} + +func HandleExtraSpecDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/flavors/1/os-extra_specs/hw:cpu_policy", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.WriteHeader(http.StatusOK) + }) +} diff --git a/openstack/compute/v2/flavors/testing/requests_test.go b/openstack/compute/v2/flavors/testing/requests_test.go new file mode 100644 index 000000000..736dceea5 --- /dev/null +++ b/openstack/compute/v2/flavors/testing/requests_test.go @@ -0,0 +1,253 @@ +package testing + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/flavors" + "github.com/huaweicloud/golangsdk/pagination" + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +const tokenID = "blerb" + +func TestDetailFlavors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "flavors": [ + { + "id": "1", + "name": "m1.tiny", + "vcpus": 1, + "disk": 1, + "ram": 512, + "swap":"", + "os-flavor-access:is_public": true, + "OS-FLV-EXT-DATA:ephemeral": 10 + }, + { + "id": "2", + "name": "m1.small", + "vcpus": 1, + "disk": 20, + "ram": 2048, + "swap": 1000, + "os-flavor-access:is_public": true, + "OS-FLV-EXT-DATA:ephemeral": 0 + }, + { + "id": "3", + "name": "m1.medium", + "vcpus": 2, + "disk": 40, + "ram": 4096, + "swap": 1000, + "os-flavor-access:is_public": false, + "OS-FLV-EXT-DATA:ephemeral": 0 + } + ], + "flavors_links": [ + { + "href": "%s/flavors/detail?marker=2", + "rel": "next" + } + ] + } + `, th.Server.URL) + case "2": + fmt.Fprintf(w, `{ "flavors": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + pages := 0 + // Get public and private flavors + err := flavors.Detail(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := flavors.ExtractFlavors(page) + if err != nil { + return false, err + } + + expected := []flavors.Flavor{ + {ID: "1", Name: "m1.tiny", VCPUs: 1, Disk: 1, RAM: 512, Swap: 0, IsPublic: true, Ephemeral: 10}, + {ID: "2", Name: "m1.small", VCPUs: 1, Disk: 20, RAM: 2048, Swap: 1000, IsPublic: true, Ephemeral: 0}, + {ID: "3", Name: "m1.medium", VCPUs: 2, Disk: 40, RAM: 4096, Swap: 1000, IsPublic: false, Ephemeral: 0}, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Fatal(err) + } + if pages != 1 { + t.Errorf("Expected one page, got %d", pages) + } +} + +func TestListFlavors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "flavors": [ + { + "id": "1", + "name": "m1.tiny", + "vcpus": 1, + "disk": 1, + "ram": 512, + "swap":"", + "os-flavor-access:is_public": true, + "OS-FLV-EXT-DATA:ephemeral": 10 + }, + { + "id": "2", + "name": "m1.small", + "vcpus": 1, + "disk": 20, + "ram": 2048, + "swap": 1000, + "os-flavor-access:is_public": true, + "OS-FLV-EXT-DATA:ephemeral": 0 + }, + { + "id": "3", + "name": "m1.medium", + "vcpus": 2, + "disk": 40, + "ram": 4096, + "swap": 1000, + "os-flavor-access:is_public": false, + "OS-FLV-EXT-DATA:ephemeral": 0 + } + ], + "flavors_links": [ + { + "href": "%s/flavors/detail?marker=2", + "rel": "next" + } + ] + } + `, th.Server.URL) + case "2": + fmt.Fprintf(w, `{ "flavors": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + pages := 0 + // Get public and private flavors + err := flavors.Detail(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := flavors.ExtractFlavors(page) + if err != nil { + return false, err + } + + expected := []flavors.Flavor{ + {ID: "1", Name: "m1.tiny", VCPUs: 1, Disk: 1, RAM: 512, Swap: 0, IsPublic: true, Ephemeral: 10}, + {ID: "2", Name: "m1.small", VCPUs: 1, Disk: 20, RAM: 2048, Swap: 1000, IsPublic: true, Ephemeral: 0}, + {ID: "3", Name: "m1.medium", VCPUs: 2, Disk: 40, RAM: 4096, Swap: 1000, IsPublic: false, Ephemeral: 0}, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Fatal(err) + } + if pages != 1 { + t.Errorf("Expected one page, got %d", pages) + } +} + +func TestGetFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "flavor": { + "id": "1", + "name": "m1.tiny", + "disk": 1, + "ram": 512, + "vcpus": 1, + "rxtx_factor": 1, + "swap": "" + } + } + `) + }) + + actual, err := flavors.Get(fake.ServiceClient(), "12345").Extract() + if err != nil { + t.Fatalf("Unable to get flavor: %v", err) + } + + expected := &flavors.Flavor{ + ID: "1", + Name: "m1.tiny", + Disk: 1, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1, + Swap: 0, + } + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } +} + +func TestFlavorExtraSpecsList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleExtraSpecsListSuccessfully(t) + + expected := ExtraSpecs + actual, err := flavors.ListExtraSpecs(fake.ServiceClient(), "1").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go new file mode 100644 index 000000000..dcd4f236b --- /dev/null +++ b/openstack/compute/v2/flavors/urls.go @@ -0,0 +1,51 @@ +package flavors + +import "github.com/huaweicloud/golangsdk" + +func getURL(client *golangsdk.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func detailURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} + +func listURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL("flavors") +} + +// func createURL(client *golangsdk.ServiceClient) string { +// return client.ServiceURL("flavors") +// } + +// func deleteURL(client *golangsdk.ServiceClient, id string) string { +// return client.ServiceURL("flavors", id) +// } + +// func accessURL(client *golangsdk.ServiceClient, id string) string { +// return client.ServiceURL("flavors", id, "os-flavor-access") +// } + +// func accessActionURL(client *golangsdk.ServiceClient, id string) string { +// return client.ServiceURL("flavors", id, "action") +// } + +func extraSpecsListURL(client *golangsdk.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +// func extraSpecsGetURL(client *golangsdk.ServiceClient, id, key string) string { +// return client.ServiceURL("flavors", id, "os-extra_specs", key) +// } + +// func extraSpecsCreateURL(client *golangsdk.ServiceClient, id string) string { +// return client.ServiceURL("flavors", id, "os-extra_specs") +// } + +// func extraSpecUpdateURL(client *golangsdk.ServiceClient, id, key string) string { +// return client.ServiceURL("flavors", id, "os-extra_specs", key) +// } + +// func extraSpecDeleteURL(client *golangsdk.ServiceClient, id, key string) string { +// return client.ServiceURL("flavors", id, "os-extra_specs", key) +// } diff --git a/openstack/compute/v2/images/doc.go b/openstack/compute/v2/images/doc.go new file mode 100644 index 000000000..22410a79a --- /dev/null +++ b/openstack/compute/v2/images/doc.go @@ -0,0 +1,32 @@ +/* +Package images provides information and interaction with the images through +the OpenStack Compute service. + +This API is deprecated and will be removed from a future version of the Nova +API service. + +An image is a collection of files used to create or rebuild a server. +Operators provide a number of pre-built OS images by default. You may also +create custom images from cloud servers you have launched. + +Example to List Images + + listOpts := images.ListOpts{ + Limit: 2, + } + + allPages, err := images.ListDetail(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + panic(err) + } + + for _, image := range allImages { + fmt.Printf("%+v\n", image) + } +*/ +package images diff --git a/openstack/compute/v2/images/requests.go b/openstack/compute/v2/images/requests.go new file mode 100644 index 000000000..f894d5c31 --- /dev/null +++ b/openstack/compute/v2/images/requests.go @@ -0,0 +1,109 @@ +package images + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// ListDetail request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts contain options filtering Images returned from a call to ListDetail. +type ListOpts struct { + // ChangesSince filters Images based on the last changed status (in date-time + // format). + ChangesSince string `q:"changes-since"` + + // Limit limits the number of Images to return. + Limit int `q:"limit"` + + // Mark is an Image UUID at which to set a marker. + Marker string `q:"marker"` + + // Name is the name of the Image. + Name string `q:"name"` + + // Server is the name of the Server (in URL format). + Server string `q:"server"` + + // Status is the current status of the Image. + Status string `q:"status"` + + // Type is the type of image (e.g. BASE, SERVER, ALL). + Type string `q:"type"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := golangsdk.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail enumerates the available images. +func ListDetail(client *golangsdk.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ImagePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get returns data about a specific image by its ID. +func Get(client *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Delete deletes the specified image ID. +func Delete(client *golangsdk.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// IDFromName is a convienience function that returns an image's ID given its +// name. +func IDFromName(client *golangsdk.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := ListDetail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractImages(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &golangsdk.ErrResourceNotFound{} + err.ResourceType = "image" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &golangsdk.ErrMultipleResourcesFound{} + err.ResourceType = "image" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/openstack/compute/v2/images/results.go b/openstack/compute/v2/images/results.go new file mode 100644 index 000000000..742de1066 --- /dev/null +++ b/openstack/compute/v2/images/results.go @@ -0,0 +1,95 @@ +package images + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as an Image. +type GetResult struct { + golangsdk.Result +} + +// DeleteResult is the result from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} + +// Extract interprets a GetResult as an Image. +func (r GetResult) Extract() (*Image, error) { + var s struct { + Image *Image `json:"image"` + } + err := r.ExtractInto(&s) + return s.Image, err +} + +// Image represents an Image returned by the Compute API. +type Image struct { + // ID is the unique ID of an image. + ID string + + // Created is the date when the image was created. + Created string + + // MinDisk is the minimum amount of disk a flavor must have to be able + // to create a server based on the image, measured in GB. + MinDisk int + + // MinRAM is the minimum amount of RAM a flavor must have to be able + // to create a server based on the image, measured in MB. + MinRAM int + + // Name provides a human-readable moniker for the OS image. + Name string + + // The Progress and Status fields indicate image-creation status. + Progress int + + // Status is the current status of the image. + Status string + + // Update is the date when the image was updated. + Updated string + + // Metadata provides free-form key/value pairs that further describe the + // image. + Metadata map[string]interface{} +} + +// ImagePage contains a single page of all Images returne from a ListDetail +// operation. Use ExtractImages to convert it into a slice of usable structs. +type ImagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if an ImagePage contains no Image results. +func (page ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(page) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page ImagePage) NextPageURL() (string, error) { + var s struct { + Links []golangsdk.Link `json:"images_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return golangsdk.ExtractNextURL(s.Links) +} + +// ExtractImages converts a page of List results into a slice of usable Image +// structs. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} diff --git a/openstack/compute/v2/images/testing/doc.go b/openstack/compute/v2/images/testing/doc.go new file mode 100644 index 000000000..db1045153 --- /dev/null +++ b/openstack/compute/v2/images/testing/doc.go @@ -0,0 +1,2 @@ +// images unit tests +package testing diff --git a/openstack/compute/v2/images/testing/requests_test.go b/openstack/compute/v2/images/testing/requests_test.go new file mode 100644 index 000000000..1d00b238a --- /dev/null +++ b/openstack/compute/v2/images/testing/requests_test.go @@ -0,0 +1,225 @@ +package testing + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/images" + "github.com/huaweicloud/golangsdk/pagination" + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func TestListImages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "images": [ + { + "status": "ACTIVE", + "updated": "2014-09-23T12:54:56Z", + "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + "OS-EXT-IMG-SIZE:size": 476704768, + "name": "F17-x86_64-cfntools", + "created": "2014-09-23T12:54:52Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": { + "architecture": "x86_64", + "block_device_mapping": { + "guest_format": null, + "boot_index": 0, + "device_name": "/dev/vda", + "delete_on_termination": false + } + } + }, + { + "status": "ACTIVE", + "updated": "2014-09-23T12:51:43Z", + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "OS-EXT-IMG-SIZE:size": 13167616, + "name": "cirros-0.3.2-x86_64-disk", + "created": "2014-09-23T12:51:42Z", + "minDisk": 0, + "progress": 100, + "minRam": 0 + } + ] + } + `) + case "2": + fmt.Fprintf(w, `{ "images": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + pages := 0 + options := &images.ListOpts{Limit: 2} + err := images.ListDetail(fake.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := images.ExtractImages(page) + if err != nil { + return false, err + } + + expected := []images.Image{ + { + ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + Name: "F17-x86_64-cfntools", + Created: "2014-09-23T12:54:52Z", + Updated: "2014-09-23T12:54:56Z", + MinDisk: 0, + MinRAM: 0, + Progress: 100, + Status: "ACTIVE", + Metadata: map[string]interface{}{ + "architecture": "x86_64", + "block_device_mapping": map[string]interface{}{ + "guest_format": interface{}(nil), + "boot_index": float64(0), + "device_name": "/dev/vda", + "delete_on_termination": false, + }, + }, + }, + { + ID: "f90f6034-2570-4974-8351-6b49732ef2eb", + Name: "cirros-0.3.2-x86_64-disk", + Created: "2014-09-23T12:51:42Z", + Updated: "2014-09-23T12:51:43Z", + MinDisk: 0, + MinRAM: 0, + Progress: 100, + Status: "ACTIVE", + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Unexpected page contents: expected %#v, got %#v", expected, actual) + } + + return false, nil + }) + + if err != nil { + t.Fatalf("EachPage error: %v", err) + } + if pages != 1 { + t.Errorf("Expected one page, got %d", pages) + } +} + +func TestGetImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "image": { + "status": "ACTIVE", + "updated": "2014-09-23T12:54:56Z", + "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + "OS-EXT-IMG-SIZE:size": 476704768, + "name": "F17-x86_64-cfntools", + "created": "2014-09-23T12:54:52Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": { + "architecture": "x86_64", + "block_device_mapping": { + "guest_format": null, + "boot_index": 0, + "device_name": "/dev/vda", + "delete_on_termination": false + } + } + } + } + `) + }) + + actual, err := images.Get(fake.ServiceClient(), "12345678").Extract() + if err != nil { + t.Fatalf("Unexpected error from Get: %v", err) + } + + expected := &images.Image{ + Status: "ACTIVE", + Updated: "2014-09-23T12:54:56Z", + ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + Name: "F17-x86_64-cfntools", + Created: "2014-09-23T12:54:52Z", + MinDisk: 0, + Progress: 100, + MinRAM: 0, + Metadata: map[string]interface{}{ + "architecture": "x86_64", + "block_device_mapping": map[string]interface{}{ + "guest_format": interface{}(nil), + "boot_index": float64(0), + "device_name": "/dev/vda", + "delete_on_termination": false, + }, + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but got %#v", expected, actual) + } +} + +func TestNextPageURL(t *testing.T) { + var page images.ImagePage + var body map[string]interface{} + bodyString := []byte(`{"images":{"links":[{"href":"http://192.154.23.87/12345/images/image3","rel":"bookmark"}]}, "images_links":[{"href":"http://192.154.23.87/12345/images/image4","rel":"next"}]}`) + err := json.Unmarshal(bodyString, &body) + if err != nil { + t.Fatalf("Error unmarshaling data into page body: %v", err) + } + page.Body = body + + expected := "http://192.154.23.87/12345/images/image4" + actual, err := page.NextPageURL() + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) +} + +// Test Image delete +func TestDeleteImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := images.Delete(fake.ServiceClient(), "12345678") + th.AssertNoErr(t, res.Err) +} diff --git a/openstack/compute/v2/images/urls.go b/openstack/compute/v2/images/urls.go new file mode 100644 index 000000000..47b03fca7 --- /dev/null +++ b/openstack/compute/v2/images/urls.go @@ -0,0 +1,15 @@ +package images + +import "github.com/huaweicloud/golangsdk" + +func listDetailURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL("images", "detail") +} + +func getURL(client *golangsdk.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} + +func deleteURL(client *golangsdk.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} diff --git a/openstack/compute/v2/servers/doc.go b/openstack/compute/v2/servers/doc.go new file mode 100644 index 000000000..3b0ab7836 --- /dev/null +++ b/openstack/compute/v2/servers/doc.go @@ -0,0 +1,115 @@ +/* +Package servers provides information and interaction with the server API +resource in the OpenStack Compute service. + +A server is a virtual machine instance in the compute system. In order for +one to be provisioned, a valid flavor and image are required. + +Example to List Servers + + listOpts := servers.ListOpts{ + AllTenants: true, + } + + allPages, err := servers.List(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to Create a Server + + createOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + err := servers.Delete(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Force Delete a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + err := servers.ForceDelete(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Reboot a Server + + rebootOpts := servers.RebootOpts{ + Type: servers.SoftReboot, + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + err := servers.Reboot(computeClient, serverID, rebootOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Rebuild a Server + + rebuildOpts := servers.RebuildOpts{ + Name: "new_name", + ImageID: "image-uuid", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + server, err := servers.Rebuilt(computeClient, serverID, rebuildOpts).Extract() + if err != nil { + panic(err) + } + +Example to Resize a Server + + resizeOpts := servers.ResizeOpts{ + FlavorRef: "flavor-uuid", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + err := servers.Resize(computeClient, serverID, resizeOpts).ExtractErr() + if err != nil { + panic(err) + } + + err = servers.ConfirmResize(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Snapshot a Server + + snapshotOpts := servers.CreateImageOpts{ + Name: "snapshot_name", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + image, err := servers.CreateImage(computeClient, serverID, snapshotOpts).ExtractImageID() + if err != nil { + panic(err) + } +*/ +package servers diff --git a/openstack/compute/v2/servers/errors.go b/openstack/compute/v2/servers/errors.go new file mode 100644 index 000000000..ce702962e --- /dev/null +++ b/openstack/compute/v2/servers/errors.go @@ -0,0 +1,71 @@ +package servers + +import ( + "fmt" + + "github.com/huaweicloud/golangsdk" +) + +// ErrNeitherImageIDNorImageNameProvided is the error when neither the image +// ID nor the image name is provided for a server operation +type ErrNeitherImageIDNorImageNameProvided struct{ golangsdk.ErrMissingInput } + +func (e ErrNeitherImageIDNorImageNameProvided) Error() string { + return "One and only one of the image ID and the image name must be provided." +} + +// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor +// ID nor the flavor name is provided for a server operation +type ErrNeitherFlavorIDNorFlavorNameProvided struct{ golangsdk.ErrMissingInput } + +func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string { + return "One and only one of the flavor ID and the flavor name must be provided." +} + +type ErrNoClientProvidedForIDByName struct{ golangsdk.ErrMissingInput } + +func (e ErrNoClientProvidedForIDByName) Error() string { + return "A service client must be provided to find a resource ID by name." +} + +// ErrInvalidHowParameterProvided is the error when an unknown value is given +// for the `how` argument +type ErrInvalidHowParameterProvided struct{ golangsdk.ErrInvalidInput } + +// ErrNoAdminPassProvided is the error when an administrative password isn't +// provided for a server operation +type ErrNoAdminPassProvided struct{ golangsdk.ErrMissingInput } + +// ErrNoImageIDProvided is the error when an image ID isn't provided for a server +// operation +type ErrNoImageIDProvided struct{ golangsdk.ErrMissingInput } + +// ErrNoIDProvided is the error when a server ID isn't provided for a server +// operation +type ErrNoIDProvided struct{ golangsdk.ErrMissingInput } + +// ErrServer is a generic error type for servers HTTP operations. +type ErrServer struct { + golangsdk.ErrUnexpectedResponseCode + ID string +} + +func (se ErrServer) Error() string { + return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID) +} + +// Error404 overrides the generic 404 error message. +func (se ErrServer) Error404(e golangsdk.ErrUnexpectedResponseCode) error { + se.ErrUnexpectedResponseCode = e + return &ErrServerNotFound{se} +} + +// ErrServerNotFound is the error when a 404 is received during server HTTP +// operations. +type ErrServerNotFound struct { + ErrServer +} + +func (e ErrServerNotFound) Error() string { + return fmt.Sprintf("I couldn't find server [%s]", e.ID) +} diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go new file mode 100644 index 000000000..e61abc9e3 --- /dev/null +++ b/openstack/compute/v2/servers/requests.go @@ -0,0 +1,860 @@ +package servers + +import ( + "encoding/base64" + "encoding/json" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/flavors" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/images" + "github.com/huaweicloud/golangsdk/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // ChangesSince is a time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Image is the name of the image in URL format. + Image string `q:"image"` + + // Flavor is the name of the flavor in URL format. + Flavor string `q:"flavor"` + + // Name of the server as a string; can be queried with regular expressions. + // Realize that ?name=bob returns both bob and bobb. If you need to match bob + // only, you can use a regular expression matching the syntax of the + // underlying database server implemented for Compute. + Name string `q:"name"` + + // Status is the value of the status of the server so that you can filter on + // "ACTIVE" for example. + Status string `q:"status"` + + // Host is the name of the host as a string. + Host string `q:"host"` + + // Marker is a UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Limit is an integer value for the limit of values to return. + Limit int `q:"limit"` + + // AllTenants is a bool to show all tenants. + //AllTenants bool `q:"all_tenants"` + + // TenantID lists servers for a particular tenant. + // Setting "AllTenants = true" is required. + //TenantID string `q:"tenant_id"` + + // NotTags filt out with tags + NotTags string `q:"not-tags"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := golangsdk.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail makes a request against the API to list servers accessible to you. +func ListDetail(client *golangsdk.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// List makes a request against the API to list servers accessible to you. +func List(client *golangsdk.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// Network is used within CreateOpts to control a new server's network +// attachments. +type Network struct { + // UUID of a network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP specifies a fixed IPv4 address to be used on this network. + FixedIP string +} + +// Personality is an array of files that are injected into the server at launch. +type Personality []*File + +// File is used within CreateOpts and RebuildOpts to inject a file into the +// server at launch. +// File implements the json.Marshaler interface, so when a Create or Rebuild +// operation is requested, json.Marshal will call File's MarshalJSON method. +type File struct { + // Path of the file. + Path string + + // Contents of the file. Maximum content size is 255 bytes. + Contents []byte +} + +// BlockDeviceDescription is used with in CreateOpts to description block device +type BlockDeviceDescription struct { + // SourceType is the source type of this block device, currently its value + // should be one of volume, image and snapshot + SourceType string `json:"source_type"` + + // DestinationType is the current type of block device, for now only support + // volume + DestinationType string `json:"destination_type"` + + // GuestFormat is the local file system format, such as swap or ext4 + GuestFormat string `json:"guest_format,omitempty"` + + // DeviceName is the device name + DeviceName string `json:"device_name,omitempty"` + + // DeleteOnTermination is a mark, whether or not delete this device while + // deleting computing service + DeleteOnTermination bool `json:"delete_on_termination,omitempty"` + + // BootIndex is the mark of boot device, 0 means this is a boot device, + // -1 not + BootIndex string `json:"boot_index"` + + // UUID is the uuid of this volume + UUID string `json:"uuid"` + + // VolumeSize is the volume size + VolumeSize string `json:"volume_size,omitempty"` + + // VolumeType is the volume type + VolumeType string `json:"volume_type,omitempty"` +} + +// MarshalJSON marshals the escaped file, base64 encoding the contents. +func (f *File) MarshalJSON() ([]byte, error) { + file := struct { + Path string `json:"path"` + Contents string `json:"contents"` + }{ + Path: f.Path, + Contents: base64.StdEncoding.EncodeToString(f.Contents), + } + return json.Marshal(file) +} + +// CreateOpts specifies server creation parameters. +type CreateOpts struct { + // Name is the name to assign to the newly launched server. + Name string `json:"name" required:"true"` + + // ImageRef [optional; required if ImageName is not provided] is the ID or + // full URL to the image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageRef string `json:"imageRef"` + + // ImageName [optional; required if ImageRef is not provided] is the name of + // the image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageName string `json:"-"` + + // FlavorRef [optional; required if FlavorName is not provided] is the ID or + // full URL to the flavor that describes the server's specs. + FlavorRef string `json:"flavorRef"` + + // FlavorName [optional; required if FlavorRef is not provided] is the name of + // the flavor that describes the server's specs. + FlavorName string `json:"-"` + + // SecurityGroups lists the names of the security groups to which this server + // should belong. + SecurityGroups []string `json:"-"` + + // UserData contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you, if it isn't already. + UserData []byte `json:"-"` + + // AvailabilityZone in which to launch the server. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // Networks dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the + // tenant. + Networks []Network `json:"-"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to the + // server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` + + // ConfigDrive enables metadata injection through a configuration drive. + ConfigDrive *bool `json:"config_drive,omitempty"` + + // AdminPass sets the root user password. If not set, a randomly-generated + // password will be created and returned in the response. + AdminPass string `json:"adminPass,omitempty"` + + // AccessIPv4 specifies an IPv4 address for the instance. + //AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 pecifies an IPv6 address for the instance. + //AccessIPv6 string `json:"accessIPv6,omitempty"` + + // ServiceClient will allow calls to be made to retrieve an image or + // flavor ID by name. + ServiceClient *golangsdk.ServiceClient `json:"-"` + + // BlockDeviceMappingV2 represents a list of descriptions of the block + // storage device + BlockDeviceMappingV2 []*BlockDeviceDescription `json:"block_device_mapping_v2,omitempty"` + + // KeyName is the name of keypair + KeyName string `json:"key_name,omitempty"` + + // ReturnReservationID specify whether this is suitable of ECS + // reservation_id or not + ReturnReservationID bool `json:"return_reservation_id,omitempty"` + + // MinCount the minimal number of creation, default 1 + MinCount int `json:"min_count,omitempty"` + + // MaxCount the maximal number of creation, default same to MinCount + MaxCount int `json:"max_count,omitempty"` +} + +// ToServerCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + sc := opts.ServiceClient + opts.ServiceClient = nil + b, err := golangsdk.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.UserData != nil { + var userData string + if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil { + userData = base64.StdEncoding.EncodeToString(opts.UserData) + } else { + userData = string(opts.UserData) + } + b["user_data"] = &userData + } + + if len(opts.SecurityGroups) > 0 { + securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups)) + for i, groupName := range opts.SecurityGroups { + securityGroups[i] = map[string]interface{}{"name": groupName} + } + b["security_groups"] = securityGroups + } + + if len(opts.Networks) > 0 { + networks := make([]map[string]interface{}, len(opts.Networks)) + for i, net := range opts.Networks { + networks[i] = make(map[string]interface{}) + if net.UUID != "" { + networks[i]["uuid"] = net.UUID + } + if net.Port != "" { + networks[i]["port"] = net.Port + } + if net.FixedIP != "" { + networks[i]["fixed_ip"] = net.FixedIP + } + } + b["networks"] = networks + } + + // If ImageRef isn't provided, check if ImageName was provided to ascertain + // the image ID. + if opts.ImageRef == "" { + if opts.ImageName != "" { + if sc == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + imageID, err := images.IDFromName(sc, opts.ImageName) + if err != nil { + return nil, err + } + b["imageRef"] = imageID + } + } + + // If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID. + if opts.FlavorRef == "" { + if opts.FlavorName == "" { + err := ErrNeitherFlavorIDNorFlavorNameProvided{} + err.Argument = "FlavorRef/FlavorName" + return nil, err + } + if sc == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + flavorID, err := flavors.IDFromName(sc, opts.FlavorName) + if err != nil { + return nil, err + } + b["flavorRef"] = flavorID + } + + return map[string]interface{}{"server": b}, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + reqBody, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), reqBody, &r.Body, nil) + return +} + +// Delete requests that a server previously provisioned be removed from your +// account. +func Delete(client *golangsdk.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// ForceDelete forces the deletion of a server. +func ForceDelete(client *golangsdk.ServiceClient, id string) (r ActionResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil) + return +} + +// Get requests details on a single server, by ID. +func Get(client *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200, 203}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// server. +type UpdateOpts struct { + // Name changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name string `json:"name,omitempty"` + + // AccessIPv4 provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "server") +} + +// Update requests that various attributes of the indicated server be changed. +// func Update(client *golangsdk.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { +// b, err := opts.ToServerUpdateMap() +// if err != nil { +// r.Err = err +// return +// } +// _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &golangsdk.RequestOpts{ +// OkCodes: []int{200}, +// }) +// return +// } + +// ChangeAdminPassword alters the administrator or root password for a specified +// server. +func ChangeAdminPassword(client *golangsdk.ServiceClient, id, newPassword string) (r ActionResult) { + b := map[string]interface{}{ + "changePassword": map[string]string{ + "adminPass": newPassword, + }, + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// RebootMethod describes the mechanisms by which a server reboot can be requested. +type RebootMethod string + +// These constants determine how a server should be rebooted. +// See the Reboot() function for further details. +const ( + SoftReboot RebootMethod = "SOFT" + HardReboot RebootMethod = "HARD" + OSReboot = SoftReboot + PowerCycle = HardReboot +) + +// RebootOptsBuilder allows extensions to add additional parameters to the +// reboot request. +type RebootOptsBuilder interface { + ToServerRebootMap() (map[string]interface{}, error) +} + +// RebootOpts provides options to the reboot request. +type RebootOpts struct { + // Type is the type of reboot to perform on the server. + Type RebootMethod `json:"type" required:"true"` +} + +// ToServerRebootMap builds a body for the reboot request. +func (opts *RebootOpts) ToServerRebootMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "reboot") +} + +/* + Reboot requests that a given server reboot. + + Two methods exist for rebooting a server: + + HardReboot (aka PowerCycle) starts the server instance by physically cutting + power to the machine, or if a VM, terminating it at the hypervisor level. + It's done. Caput. Full stop. + Then, after a brief while, power is rtored or the VM instance restarted. + + SoftReboot (aka OSReboot) simply tells the OS to restart under its own + procedure. + E.g., in Linux, asking it to enter runlevel 6, or executing + "sudo shutdown -r now", or by asking Windows to rtart the machine. +*/ +func Reboot(client *golangsdk.ServiceClient, id string, opts RebootOptsBuilder) (r ActionResult) { + b, err := opts.ToServerRebootMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// RebuildOptsBuilder allows extensions to provide additional parameters to the +// rebuild request. +type RebuildOptsBuilder interface { + ToServerRebuildMap() (map[string]interface{}, error) +} + +// RebuildOpts represents the configuration options used in a server rebuild +// operation. +type RebuildOpts struct { + // AdminPass is the server's admin password + AdminPass string `json:"adminPass,omitempty"` + + // ImageID is the ID of the image you want your server to be provisioned on. + ImageID string `json:"imageRef"` + + // ImageName is readable name of an image. + ImageName string `json:"-"` + + // Name to set the server to + Name string `json:"name,omitempty"` + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) + // to attach to the server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality [optional] includes files to inject into the server at launch. + // Rebuild will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` + + // ServiceClient will allow calls to be made to retrieve an image or + // flavor ID by name. + ServiceClient *golangsdk.ServiceClient `json:"-"` +} + +// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON +func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { + b, err := golangsdk.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + // If ImageRef isn't provided, check if ImageName was provided to ascertain + // the image ID. + if opts.ImageID == "" { + if opts.ImageName != "" { + if opts.ServiceClient == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + imageID, err := images.IDFromName(opts.ServiceClient, opts.ImageName) + if err != nil { + return nil, err + } + b["imageRef"] = imageID + } + } + + return map[string]interface{}{"rebuild": b}, nil +} + +// Rebuild will reprovision the server according to the configuration options +// provided in the RebuildOpts struct. +func Rebuild(client *golangsdk.ServiceClient, id string, opts RebuildOptsBuilder) (r RebuildResult) { + b, err := opts.ToServerRebuildMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, &r.Body, nil) + return +} + +// ResizeOptsBuilder allows extensions to add additional parameters to the +// resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]interface{}, error) +} + +// ResizeOpts represents the configuration options used to control a Resize +// operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string `json:"flavorRef" required:"true"` +} + +// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON +// request body for the Resize request. +func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "resize") +} + +// Resize instructs the provider to change the flavor of the server. +// +// Note that this implies rebuilding it. +// +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in VERIFY_RESIZE state. +// While in this state, you can explore the use of the new server's +// configuration. If you like it, call ConfirmResize() to commit the resize +// permanently. Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *golangsdk.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) { + b, err := opts.ToServerResizeMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// ConfirmResize confirms a previous resize operation on a server. +// See Resize() for more details. +func ConfirmResize(client *golangsdk.ServiceClient, id string) (r ActionResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"confirmResize": nil}, nil, &golangsdk.RequestOpts{ + OkCodes: []int{201, 202, 204}, + }) + return +} + +// RevertResize cancels a previous resize operation on a server. +// See Resize() for more details. +func RevertResize(client *golangsdk.ServiceClient, id string) (r ActionResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"revertResize": nil}, nil, nil) + return +} + +// RescueOptsBuilder is an interface that allows extensions to override the +// default structure of a Rescue request. +type RescueOptsBuilder interface { + ToServerRescueMap() (map[string]interface{}, error) +} + +// RescueOpts represents the configuration options used to control a Rescue +// option. +type RescueOpts struct { + // AdminPass is the desired administrative password for the instance in + // RESCUE mode. If it's left blank, the server will generate a password. + AdminPass string `json:"adminPass,omitempty"` +} + +// ToServerRescueMap formats a RescueOpts as a map that can be used as a JSON +// request body for the Rescue request. +func (opts RescueOpts) ToServerRescueMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "rescue") +} + +// Rescue instructs the provider to place the server into RESCUE mode. +func Rescue(client *golangsdk.ServiceClient, id string, opts RescueOptsBuilder) (r RescueResult) { + b, err := opts.ToServerRescueMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ResetMetadataOptsBuilder allows extensions to add additional parameters to +// the Reset request. +type ResetMetadataOptsBuilder interface { + ToMetadataResetMap() (map[string]interface{}, error) +} + +// MetadataOpts is a map that contains key-value pairs. +type MetadataOpts map[string]string + +// ToMetadataResetMap assembles a body for a Reset request based on the contents +// of a MetadataOpts. +func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ToMetadataUpdateMap assembles a body for an Update request based on the +// contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ResetMetadata will create multiple new key-value pairs for the given server +// ID. +// Note: Using this operation will erase any already-existing metadata and +// create the new metadata provided. To keep any already-existing metadata, +// use the UpdateMetadatas or UpdateMetadata function. +func ResetMetadata(client *golangsdk.ServiceClient, id string, opts ResetMetadataOptsBuilder) (r ResetMetadataResult) { + b, err := opts.ToMetadataResetMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(metadataURL(client, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(client *golangsdk.ServiceClient, id string) (r GetMetadataResult) { + _, r.Err = client.Get(metadataURL(client, id), &r.Body, nil) + return +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Create request. +type UpdateMetadataOptsBuilder interface { + ToMetadataUpdateMap() (map[string]interface{}, error) +} + +// UpdateMetadata updates (or creates) all the metadata specified by opts for +// the given server ID. This operation does not affect already-existing metadata +// that is not specified by opts. +func UpdateMetadata(client *golangsdk.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToMetadataUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(metadataURL(client, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// MetadatumOptsBuilder allows extensions to add additional parameters to the +// Create request. +type MetadatumOptsBuilder interface { + ToMetadatumCreateMap() (map[string]interface{}, string, error) +} + +// MetadatumOpts is a map of length one that contains a key-value pair. +type MetadatumOpts map[string]string + +// ToMetadatumCreateMap assembles a body for a Create request based on the +// contents of a MetadataumOpts. +func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) { + if len(opts) != 1 { + err := golangsdk.ErrInvalidInput{} + err.Argument = "servers.MetadatumOpts" + err.Info = "Must have 1 and only 1 key-value pair" + return nil, "", err + } + metadatum := map[string]interface{}{"meta": opts} + var key string + for k := range metadatum["meta"].(MetadatumOpts) { + key = k + } + return metadatum, key, nil +} + +// CreateMetadatum will create or update the key-value pair with the given key +// for the given server ID. +func CreateMetadatum(client *golangsdk.ServiceClient, id string, opts MetadatumOptsBuilder) (r CreateMetadatumResult) { + b, key, err := opts.ToMetadatumCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(metadatumURL(client, id, key), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Metadatum requests the key-value pair with the given key for the given +// server ID. +func Metadatum(client *golangsdk.ServiceClient, id, key string) (r GetMetadatumResult) { + _, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil) + return +} + +// DeleteMetadatum will delete the key-value pair with the given key for the +// given server ID. +func DeleteMetadatum(client *golangsdk.ServiceClient, id, key string) (r DeleteMetadatumResult) { + _, r.Err = client.Delete(metadatumURL(client, id, key), nil) + return +} + +// ListAddresses makes a request against the API to list the servers IP +// addresses. +// func ListAddresses(client *golangsdk.ServiceClient, id string) pagination.Pager { +// return pagination.NewPager(client, listAddressesURL(client, id), func(r pagination.PageResult) pagination.Page { +// return AddressPage{pagination.SinglePageBase(r)} +// }) +// } + +// ListAddressesByNetwork makes a request against the API to list the servers IP +// addresses for the given network. +// func ListAddressesByNetwork(client *golangsdk.ServiceClient, id, network string) pagination.Pager { +// return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), func(r pagination.PageResult) pagination.Page { +// return NetworkAddressPage{pagination.SinglePageBase(r)} +// }) +// } + +// CreateImageOptsBuilder allows extensions to add additional parameters to the +// CreateImage request. +type CreateImageOptsBuilder interface { + ToServerCreateImageMap() (map[string]interface{}, error) +} + +// CreateImageOpts provides options to pass to the CreateImage request. +type CreateImageOpts struct { + // Name of the image/snapshot. + Name string `json:"name" required:"true"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to + // the created image. + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToServerCreateImageMap formats a CreateImageOpts structure into a request +// body. +func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "createImage") +} + +// CreateImage makes a request against the nova API to schedule an image to be +// created of the server +func CreateImage(client *golangsdk.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) { + b, err := opts.ToServerCreateImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{202}, + }) + r.Err = err + r.Header = resp.Header + return +} + +// IDFromName is a convienience function that returns a server's ID given its +// name. +func IDFromName(client *golangsdk.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := ListDetail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractServers(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + return "", golangsdk.ErrResourceNotFound{Name: name, ResourceType: "server"} + case 1: + return id, nil + default: + return "", golangsdk.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "server"} + } +} + +// GetPassword makes a request against the nova API to get the encrypted +// administrative password. +// func GetPassword(client *golangsdk.ServiceClient, serverId string) (r GetPasswordResult) { +// _, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil) +// return +// } diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go new file mode 100644 index 000000000..5b55eaca3 --- /dev/null +++ b/openstack/compute/v2/servers/results.go @@ -0,0 +1,414 @@ +package servers + +import ( + "crypto/rsa" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "path" + "time" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +type serverResult struct { + golangsdk.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + var s Server + err := r.ExtractInto(&s) + return &s, err +} + +func (r serverResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "server") +} + +func ExtractServersInto(r pagination.Page, v interface{}) error { + return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers") +} + +// CreateResult is the response from a Create operation. Call its Extract +// method to interpret it as a Server. +type CreateResult struct { + serverResult +} + +// GetResult is the response from a Get operation. Call its Extract +// method to interpret it as a Server. +type GetResult struct { + serverResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Server. +type UpdateResult struct { + serverResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} + +// RebuildResult is the response from a Rebuild operation. Call its Extract +// method to interpret it as a Server. +type RebuildResult struct { + serverResult +} + +// ActionResult represents the result of server action operations, like reboot. +// Call its ExtractErr method to determine if the action succeeded or failed. +type ActionResult struct { + golangsdk.ErrResult +} + +// RescueResult is the response from a Rescue operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type RescueResult struct { + ActionResult +} + +// CreateImageResult is the response from a CreateImage operation. Call its +// ExtractImageID method to retrieve the ID of the newly created image. +type CreateImageResult struct { + golangsdk.Result +} + +// GetPasswordResult represent the result of a get os-server-password operation. +// Call its ExtractPassword method to retrieve the password. +type GetPasswordResult struct { + golangsdk.Result +} + +// ExtractPassword gets the encrypted password. +// If privateKey != nil the password is decrypted with the private key. +// If privateKey == nil the encrypted password is returned and can be decrypted +// with: +// echo '' | base64 -D | openssl rsautl -decrypt -inkey +func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) { + var s struct { + Password string `json:"password"` + } + err := r.ExtractInto(&s) + if err == nil && privateKey != nil && s.Password != "" { + return decryptPassword(s.Password, privateKey) + } + return s.Password, err +} + +func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) { + b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword))) + + n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword)) + if err != nil { + return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err) + } + password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n]) + if err != nil { + return "", fmt.Errorf("Failed to decrypt password: %s", err) + } + + return string(password), nil +} + +// ExtractImageID gets the ID of the newly created server image from the header. +func (r CreateImageResult) ExtractImageID() (string, error) { + if r.Err != nil { + return "", r.Err + } + // Get the image id from the header + u, err := url.ParseRequestURI(r.Header.Get("Location")) + if err != nil { + return "", err + } + imageID := path.Base(u.Path) + if imageID == "." || imageID == "/" { + return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u) + } + return imageID, nil +} + +// Extract interprets any RescueResult as an AdminPass, if possible. +func (r RescueResult) Extract() (string, error) { + var s struct { + AdminPass string `json:"adminPass"` + } + err := r.ExtractInto(&s) + return s.AdminPass, err +} + +// Server represents a server/instance in the OpenStack cloud. +type Server struct { + // ID uniquely identifies this server amongst all other servers, + // including those not accessible to the current tenant. + ID string `json:"id"` + + // TenantID identifies the tenant owning this server resource. + TenantID string `json:"tenant_id"` + + // UserID uniquely identifies the user account owning the tenant. + UserID string `json:"user_id"` + + // Name contains the human-readable name for the server. + Name string `json:"name"` + + // Updated and Created contain ISO-8601 timestamps of when the state of the + // server last changed, and when it was created. + Updated time.Time `json:"updated"` + Created time.Time `json:"created"` + + // HostID is the host where the server is located in the cloud. + HostID string `json:"hostid"` + + // Status contains the current operational status of the server, + // such as IN_PROGRESS or ACTIVE. + Status string `json:"status"` + + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int `json:"progress"` + + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, + // suitable for remote access for administration. + AccessIPv4 string `json:"accessIPv4"` + AccessIPv6 string `json:"accessIPv6"` + + // Image refers to a JSON object, which itself indicates the OS image used to + // deploy the server. + Image map[string]interface{} `json:"-"` + + // Flavor refers to a JSON object, which itself indicates the hardware + // configuration of the deployed server. + Flavor map[string]interface{} `json:"flavor"` + + // Addresses includes a list of all IP addresses assigned to the server, + // keyed by pool. + Addresses map[string]interface{} `json:"addresses"` + + // Metadata includes a list of all user-specified key-value pairs attached + // to the server. + Metadata map[string]string `json:"metadata"` + + // Links includes HTTP references to the itself, useful for passing along to + // other APIs that might want a server reference. + Links []interface{} `json:"links"` + + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name"` + + // AdminPass will generally be empty (""). However, it will contain the + // administrative password chosen when provisioning a new server without a + // set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass"` + + // SecurityGroups includes the security groups that this instance has applied + // to it. + SecurityGroups []map[string]interface{} `json:"security_groups"` + + // Fault contains failure information about a server. + Fault Fault `json:"fault"` +} + +type Fault struct { + Code int `json:"code"` + Created time.Time `json:"created"` + Details string `json:"details"` + Message string `json:"message"` +} + +func (r *Server) UnmarshalJSON(b []byte) error { + type tmp Server + var s struct { + tmp + Image interface{} `json:"image"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Server(s.tmp) + + switch t := s.Image.(type) { + case map[string]interface{}: + r.Image = t + case string: + switch t { + case "": + r.Image = nil + } + } + + return err +} + +// ServerPage abstracts the raw results of making a List() request against +// the API. As OpenStack extensions may freely alter the response bodies of +// structures returned to the client, you may only safely access the data +// provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r ServerPage) IsEmpty() (bool, error) { + s, err := ExtractServers(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r ServerPage) NextPageURL() (string, error) { + var s struct { + Links []golangsdk.Link `json:"servers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return golangsdk.ExtractNextURL(s.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, +// producing a slice of Server entities. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s []Server + err := ExtractServersInto(r, &s) + return s, err +} + +// MetadataResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type MetadataResult struct { + golangsdk.Result +} + +// GetMetadataResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult contains the result of a Reset operation. Call its +// Extract method to interpret it as a map[string]interface. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult contains the result of an Update operation. Call its +// Extract method to interpret it as a map[string]interface. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single +// key-value pair. +type MetadatumResult struct { + golangsdk.Result +} + +// GetMetadatumResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult contains the result of a Create operation. Call its +// Extract method to interpret it as a map[string]interface. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult contains the result of a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DeleteMetadatumResult struct { + golangsdk.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + var s struct { + Metadata map[string]string `json:"metadata"` + } + err := r.ExtractInto(&s) + return s.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + var s struct { + Metadatum map[string]string `json:"meta"` + } + err := r.ExtractInto(&s) + return s.Metadatum, err +} + +// Address represents an IP address. +type Address struct { + Version int `json:"version"` + Address string `json:"addr"` +} + +// AddressPage abstracts the raw results of making a ListAddresses() request +// against the API. As OpenStack extensions may freely alter the response bodies +// of structures returned to the client, you may only safely access the data +// provided through the ExtractAddresses call. +type AddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if an AddressPage contains no networks. +func (r AddressPage) IsEmpty() (bool, error) { + addresses, err := ExtractAddresses(r) + return len(addresses) == 0, err +} + +// ExtractAddresses interprets the results of a single page from a +// ListAddresses() call, producing a map of addresses. +func ExtractAddresses(r pagination.Page) (map[string][]Address, error) { + var s struct { + Addresses map[string][]Address `json:"addresses"` + } + err := (r.(AddressPage)).ExtractInto(&s) + return s.Addresses, err +} + +// NetworkAddressPage abstracts the raw results of making a +// ListAddressesByNetwork() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures +// returned to the client, you may only safely access the data provided through +// the ExtractAddresses call. +type NetworkAddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a NetworkAddressPage contains no addresses. +func (r NetworkAddressPage) IsEmpty() (bool, error) { + addresses, err := ExtractNetworkAddresses(r) + return len(addresses) == 0, err +} + +// ExtractNetworkAddresses interprets the results of a single page from a +// ListAddressesByNetwork() call, producing a slice of addresses. +func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) { + var s map[string][]Address + err := (r.(NetworkAddressPage)).ExtractInto(&s) + if err != nil { + return nil, err + } + + var key string + for k := range s { + key = k + } + + return s[key], err +} diff --git a/openstack/compute/v2/servers/testing/doc.go b/openstack/compute/v2/servers/testing/doc.go new file mode 100644 index 000000000..b3fee3aac --- /dev/null +++ b/openstack/compute/v2/servers/testing/doc.go @@ -0,0 +1,2 @@ +// servers unit tests +package testing diff --git a/openstack/compute/v2/servers/testing/fixtures.go b/openstack/compute/v2/servers/testing/fixtures.go new file mode 100644 index 000000000..20fd934e9 --- /dev/null +++ b/openstack/compute/v2/servers/testing/fixtures.go @@ -0,0 +1,1075 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +// ServerListBody contains the canned body of a servers.List response. +const ServerListBody = ` +{ + "servers": [ + { + "status": "ACTIVE", + "updated": "2014-09-25T13:10:10Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": 4, + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001e", + "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "herp", + "created": "2014-09-25T13:10:02Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": "", + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682bb", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "merp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } + ] +} +` + +// SingleServerBody is the canned body of a Get request on an existing server. +const SingleServerBody = ` +{ + "server": { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } +} +` + +// FaultyServerBody is the body of a Get request on an existing server +// which has a fault/error. +const FaultyServerBody = ` +{ + "server": { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {}, + "fault": { + "message": "Conflict updating instance c2ce4dea-b73f-4d01-8633-2c6032869281. Expected: {'task_state': [u'spawning']}. Actual: {'task_state': None}", + "code": 500, + "created": "2017-11-11T07:58:39Z", + "details": "Stock details for test" + } + } +} +` + +const ServerPasswordBody = ` +{ + "password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg==" +} +` + +var ( + herpTimeCreated, _ = time.Parse(time.RFC3339, "2014-09-25T13:10:02Z") + herpTimeUpdated, _ = time.Parse(time.RFC3339, "2014-09-25T13:10:10Z") + // ServerHerp is a Server struct that should correspond to the first result in ServerListBody. + ServerHerp = servers.Server{ + Status: "ACTIVE", + Updated: herpTimeUpdated, + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": float64(4), + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark", + }, + }, + Image: map[string]interface{}{ + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "herp", + Created: herpTimeCreated, + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]string{}, + SecurityGroups: []map[string]interface{}{ + { + "name": "default", + }, + }, + } + + derpTimeCreated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:41Z") + derpTimeUpdated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:49Z") + // ServerDerp is a Server struct that should correspond to the second server in ServerListBody. + ServerDerp = servers.Server{ + Status: "ACTIVE", + Updated: derpTimeUpdated, + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": float64(4), + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark", + }, + }, + Image: map[string]interface{}{ + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "derp", + Created: derpTimeCreated, + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]string{}, + SecurityGroups: []map[string]interface{}{ + { + "name": "default", + }, + }, + } + + merpTimeCreated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:41Z") + merpTimeUpdated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:49Z") + // ServerMerp is a Server struct that should correspond to the second server in ServerListBody. + ServerMerp = servers.Server{ + Status: "ACTIVE", + Updated: merpTimeUpdated, + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": float64(4), + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark", + }, + }, + Image: nil, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "9e5476bd-a4ec-4653-93d6-72c93aa682bb", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "merp", + Created: merpTimeCreated, + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]string{}, + SecurityGroups: []map[string]interface{}{ + { + "name": "default", + }, + }, + } + + faultTimeCreated, _ = time.Parse(time.RFC3339, "2017-11-11T07:58:39Z") + DerpFault = servers.Fault{ + Code: 500, + Created: faultTimeCreated, + Details: "Stock details for test", + Message: "Conflict updating instance c2ce4dea-b73f-4d01-8633-2c6032869281. " + + "Expected: {'task_state': [u'spawning']}. Actual: {'task_state': None}", + } +) + +type CreateOptsWithCustomField struct { + servers.CreateOpts + Foo string `json:"foo,omitempty"` +} + +func (opts CreateOptsWithCustomField) ToServerCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "server") +} + +// HandleServerCreationSuccessfully sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) + + th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "images": [ + { + "status": "ACTIVE", + "updated": "2014-09-23T12:54:56Z", + "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + "OS-EXT-IMG-SIZE:size": 476704768, + "name": "F17-x86_64-cfntools", + "created": "2014-09-23T12:54:52Z", + "minDisk": 0, + "progress": 100, + "minRam": 0 + }, + { + "status": "ACTIVE", + "updated": "2014-09-23T12:51:43Z", + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "OS-EXT-IMG-SIZE:size": 13167616, + "name": "cirros-0.3.2-x86_64-disk", + "created": "2014-09-23T12:51:42Z", + "minDisk": 0, + "progress": 100, + "minRam": 0 + } + ] + } + `) + case "2": + fmt.Fprintf(w, `{ "images": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "flavors": [ + { + "id": "1", + "name": "m1.tiny", + "disk": 1, + "ram": 512, + "vcpus": 1, + "swap":"" + }, + { + "id": "2", + "name": "m2.small", + "disk": 10, + "ram": 1024, + "vcpus": 2, + "swap": 1000 + } + ], + "flavors_links": [ + { + "href": "%s/flavors/detail?marker=2", + "rel": "next" + } + ] + } + `, th.Server.URL) + case "2": + fmt.Fprintf(w, `{ "flavors": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleServerCreationWithCustomFieldSuccessfully sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationWithCustomFieldSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1", + "foo": "bar" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerCreationWithUserdata sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationWithUserdata(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1", + "user_data": "dXNlcmRhdGEgc3RyaW5n" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerCreationWithMetadata sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationWithMetadata(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1", + "metadata": { + "abc": "def" + } + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerListSuccessfully sets up the test server to respond to a server List request. +func HandleServerListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ServerListBody) + case "9e5476bd-a4ec-4653-93d6-72c93aa682ba": + fmt.Fprintf(w, `{ "servers": [] }`) + default: + t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandleServerDeletionSuccessfully sets up the test server to respond to a server deletion request. +func HandleServerDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server force deletion +// request. +func HandleServerForceDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "forceDelete": "" }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleServerGetSuccessfully sets up the test server to respond to a server Get request. +func HandleServerGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SingleServerBody) + }) +} + +// HandleServerGetFaultSuccessfully sets up the test server to respond to a server Get +// request which contains a fault. +func HandleServerGetFaultSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, FaultyServerBody) + }) +} + +// HandleServerUpdateSuccessfully sets up the test server to respond to a server Update request. +func HandleServerUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`) + + fmt.Fprintf(w, SingleServerBody) + }) +} + +// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password +// change request. +func HandleAdminPasswordChangeSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleRebootSuccessfully sets up the test server to respond to a reboot request with success. +func HandleRebootSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleRebuildSuccessfully sets up the test server to respond to a rebuild request with success. +func HandleRebuildSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "rebuild": { + "name": "new-name", + "adminPass": "swordfish", + "imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "accessIPv4": "1.2.3.4" + } + } + `) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerRescueSuccessfully sets up the test server to respond to a server Rescue request. +func HandleServerRescueSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "rescue": { "adminPass": "1234567890" } }`) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ "adminPass": "1234567890" }`)) + }) +} + +// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request. +func HandleMetadatumGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "meta": {"foo":"bar"}}`)) + }) +} + +// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request. +func HandleMetadatumCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "meta": { + "foo": "bar" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "meta": {"foo":"bar"}}`)) + }) +} + +// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request. +func HandleMetadatumDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request. +func HandleMetadataGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`)) + }) +} + +// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request. +func HandleMetadataResetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "metadata": { + "foo": "bar", + "this": "that" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`)) + }) +} + +// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request. +func HandleMetadataUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "metadata": { + "foo": "baz", + "this": "those" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "metadata": {"foo":"baz", "this":"those"}}`)) + }) +} + +// ListAddressesExpected represents an expected repsonse from a ListAddresses request. +var ListAddressesExpected = map[string][]servers.Address{ + "public": { + { + Version: 4, + Address: "50.56.176.35", + }, + { + Version: 6, + Address: "2001:4800:790e:510:be76:4eff:fe04:84a8", + }, + }, + "private": { + { + Version: 4, + Address: "10.180.3.155", + }, + }, +} + +// HandleAddressListSuccessfully sets up the test server to respond to a ListAddresses request. +func HandleAddressListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf/ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "addresses": { + "public": [ + { + "version": 4, + "addr": "50.56.176.35" + }, + { + "version": 6, + "addr": "2001:4800:790e:510:be76:4eff:fe04:84a8" + } + ], + "private": [ + { + "version": 4, + "addr": "10.180.3.155" + } + ] + } + }`) + }) +} + +// ListNetworkAddressesExpected represents an expected repsonse from a ListAddressesByNetwork request. +var ListNetworkAddressesExpected = []servers.Address{ + { + Version: 4, + Address: "50.56.176.35", + }, + { + Version: 6, + Address: "2001:4800:790e:510:be76:4eff:fe04:84a8", + }, +} + +// HandleNetworkAddressListSuccessfully sets up the test server to respond to a ListAddressesByNetwork request. +func HandleNetworkAddressListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf/ips/public", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "public": [ + { + "version": 4, + "addr": "50.56.176.35" + }, + { + "version": 6, + "addr": "2001:4800:790e:510:be76:4eff:fe04:84a8" + } + ] + }`) + }) +} + +// HandleCreateServerImageSuccessfully sets up the test server to respond to a TestCreateServerImage request. +func HandleCreateServerImageSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/serverimage/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.Header().Add("Location", "https://0.0.0.0/images/xxxx-xxxxx-xxxxx-xxxx") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandlePasswordGetSuccessfully sets up the test server to respond to a password Get request. +func HandlePasswordGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/os-server-password", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, ServerPasswordBody) + }) +} diff --git a/openstack/compute/v2/servers/testing/requests_test.go b/openstack/compute/v2/servers/testing/requests_test.go new file mode 100644 index 000000000..a314a8ae0 --- /dev/null +++ b/openstack/compute/v2/servers/testing/requests_test.go @@ -0,0 +1,552 @@ +package testing + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "testing" + + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/availabilityzones" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/diskconfig" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/extendedstatus" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + "github.com/huaweicloud/golangsdk/pagination" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerListSuccessfully(t) + + pages := 0 + err := servers.ListDetail(client.ServiceClient(), servers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + + if len(actual) != 3 { + t.Fatalf("Expected 3 servers, got %d", len(actual)) + } + th.CheckDeepEquals(t, ServerHerp, actual[0]) + th.CheckDeepEquals(t, ServerDerp, actual[1]) + th.CheckDeepEquals(t, ServerMerp, actual[2]) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListAllServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerListSuccessfully(t) + + allPages, err := servers.ListDetail(client.ServiceClient(), servers.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := servers.ExtractServers(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ServerHerp, actual[0]) + th.CheckDeepEquals(t, ServerDerp, actual[1]) +} + +func TestListAllServersWithExtensions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerListSuccessfully(t) + + type ServerWithExt struct { + servers.Server + availabilityzones.ServerAvailabilityZoneExt + extendedstatus.ServerExtendedStatusExt + diskconfig.ServerDiskConfigExt + } + + allPages, err := servers.ListDetail(client.ServiceClient(), servers.ListOpts{}).AllPages() + th.AssertNoErr(t, err) + + var actual []ServerWithExt + err = servers.ExtractServersInto(allPages, &actual) + th.AssertNoErr(t, err) + th.AssertEquals(t, 3, len(actual)) + th.AssertEquals(t, "nova", actual[0].AvailabilityZone) + th.AssertEquals(t, "RUNNING", actual[0].PowerState.String()) + th.AssertEquals(t, "", actual[0].TaskState) + th.AssertEquals(t, "active", actual[0].VmState) + th.AssertEquals(t, diskconfig.Manual, actual[0].DiskConfig) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationSuccessfully(t, SingleServerBody) + + actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestCreateServerWithCustomField(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationWithCustomFieldSuccessfully(t, SingleServerBody) + + actual, err := servers.Create(client.ServiceClient(), CreateOptsWithCustomField{ + CreateOpts: servers.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + }, + Foo: "bar", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestCreateServerWithMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationWithMetadata(t, SingleServerBody) + + actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + Metadata: map[string]string{ + "abc": "def", + }, + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestCreateServerWithUserdataString(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationWithUserdata(t, SingleServerBody) + + actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + UserData: []byte("userdata string"), + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestCreateServerWithUserdataEncoded(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationWithUserdata(t, SingleServerBody) + + encoded := base64.StdEncoding.EncodeToString([]byte("userdata string")) + + actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + UserData: []byte(encoded), + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestCreateServerWithImageNameAndFlavorName(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationSuccessfully(t, SingleServerBody) + + actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{ + Name: "derp", + ImageName: "cirros-0.3.2-x86_64-disk", + FlavorName: "m1.tiny", + ServiceClient: client.ServiceClient(), + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerDeletionSuccessfully(t) + + res := servers.Delete(client.ServiceClient(), "asdfasdfasdf") + th.AssertNoErr(t, res.Err) +} + +func TestForceDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerForceDeletionSuccessfully(t) + + res := servers.ForceDelete(client.ServiceClient(), "asdfasdfasdf") + th.AssertNoErr(t, res.Err) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerGetSuccessfully(t) + + client := client.ServiceClient() + actual, err := servers.Get(client, "1234asdf").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestGetFaultyServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerGetFaultSuccessfully(t) + + client := client.ServiceClient() + actual, err := servers.Get(client, "1234asdf").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + FaultyServer := ServerDerp + FaultyServer.Fault = DerpFault + th.CheckDeepEquals(t, FaultyServer, *actual) +} + +func TestGetServerWithExtensions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerGetSuccessfully(t) + + var s struct { + servers.Server + availabilityzones.ServerAvailabilityZoneExt + extendedstatus.ServerExtendedStatusExt + diskconfig.ServerDiskConfigExt + } + + err := servers.Get(client.ServiceClient(), "1234asdf").ExtractInto(&s) + th.AssertNoErr(t, err) + th.AssertEquals(t, "nova", s.AvailabilityZone) + th.AssertEquals(t, "RUNNING", s.PowerState.String()) + th.AssertEquals(t, "", s.TaskState) + th.AssertEquals(t, "active", s.VmState) + th.AssertEquals(t, diskconfig.Manual, s.DiskConfig) + + err = servers.Get(client.ServiceClient(), "1234asdf").ExtractInto(s) + if err == nil { + t.Errorf("Expected error when providing non-pointer struct") + } +} + +// func TestUpdateServer(t *testing.T) { +// th.SetupHTTP() +// defer th.TeardownHTTP() +// HandleServerUpdateSuccessfully(t) + +// client := client.ServiceClient() +// actual, err := servers.Update(client, "1234asdf", servers.UpdateOpts{Name: "new-name"}).Extract() +// if err != nil { +// t.Fatalf("Unexpected Update error: %v", err) +// } + +// th.CheckDeepEquals(t, ServerDerp, *actual) +// } + +func TestChangeServerAdminPassword(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAdminPasswordChangeSuccessfully(t) + + res := servers.ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password") + th.AssertNoErr(t, res.Err) +} + +// func TestGetPassword(t *testing.T) { +// th.SetupHTTP() +// defer th.TeardownHTTP() +// HandlePasswordGetSuccessfully(t) + +// res := servers.GetPassword(client.ServiceClient(), "1234asdf") +// th.AssertNoErr(t, res.Err) +// } + +func TestRebootServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRebootSuccessfully(t) + + res := servers.Reboot(client.ServiceClient(), "1234asdf", &servers.RebootOpts{ + Type: servers.SoftReboot, + }) + th.AssertNoErr(t, res.Err) +} + +func TestRebuildServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRebuildSuccessfully(t, SingleServerBody) + + opts := servers.RebuildOpts{ + Name: "new-name", + AdminPass: "swordfish", + ImageID: "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + AccessIPv4: "1.2.3.4", + } + + actual, err := servers.Rebuild(client.ServiceClient(), "1234asdf", opts).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestResizeServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`) + + w.WriteHeader(http.StatusAccepted) + }) + + res := servers.Resize(client.ServiceClient(), "1234asdf", servers.ResizeOpts{FlavorRef: "2"}) + th.AssertNoErr(t, res.Err) +} + +func TestConfirmResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "confirmResize": null }`) + + w.WriteHeader(http.StatusNoContent) + }) + + res := servers.ConfirmResize(client.ServiceClient(), "1234asdf") + th.AssertNoErr(t, res.Err) +} + +func TestRevertResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "revertResize": null }`) + + w.WriteHeader(http.StatusAccepted) + }) + + res := servers.RevertResize(client.ServiceClient(), "1234asdf") + th.AssertNoErr(t, res.Err) +} + +func TestRescue(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleServerRescueSuccessfully(t) + + res := servers.Rescue(client.ServiceClient(), "1234asdf", servers.RescueOpts{ + AdminPass: "1234567890", + }) + th.AssertNoErr(t, res.Err) + adminPass, _ := res.Extract() + th.AssertEquals(t, "1234567890", adminPass) +} + +func TestGetMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumGetSuccessfully(t) + + expected := map[string]string{"foo": "bar"} + actual, err := servers.Metadatum(client.ServiceClient(), "1234asdf", "foo").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestCreateMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumCreateSuccessfully(t) + + expected := map[string]string{"foo": "bar"} + actual, err := servers.CreateMetadatum(client.ServiceClient(), "1234asdf", servers.MetadatumOpts{"foo": "bar"}).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumDeleteSuccessfully(t) + + err := servers.DeleteMetadatum(client.ServiceClient(), "1234asdf", "foo").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataGetSuccessfully(t) + + expected := map[string]string{"foo": "bar", "this": "that"} + actual, err := servers.Metadata(client.ServiceClient(), "1234asdf").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestResetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataResetSuccessfully(t) + + expected := map[string]string{"foo": "bar", "this": "that"} + actual, err := servers.ResetMetadata(client.ServiceClient(), "1234asdf", servers.MetadataOpts{ + "foo": "bar", + "this": "that", + }).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestUpdateMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataUpdateSuccessfully(t) + + expected := map[string]string{"foo": "baz", "this": "those"} + actual, err := servers.UpdateMetadata(client.ServiceClient(), "1234asdf", servers.MetadataOpts{ + "foo": "baz", + "this": "those", + }).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +// func TestListAddresses(t *testing.T) { +// th.SetupHTTP() +// defer th.TeardownHTTP() +// HandleAddressListSuccessfully(t) + +// expected := ListAddressesExpected +// pages := 0 +// err := servers.ListAddresses(client.ServiceClient(), "asdfasdfasdf").EachPage(func(page pagination.Page) (bool, error) { +// pages++ + +// actual, err := servers.ExtractAddresses(page) +// th.AssertNoErr(t, err) + +// if len(actual) != 2 { +// t.Fatalf("Expected 2 networks, got %d", len(actual)) +// } +// th.CheckDeepEquals(t, expected, actual) + +// return true, nil +// }) +// th.AssertNoErr(t, err) +// th.CheckEquals(t, 1, pages) +// } + +// func TestListAddressesByNetwork(t *testing.T) { +// th.SetupHTTP() +// defer th.TeardownHTTP() +// HandleNetworkAddressListSuccessfully(t) + +// expected := ListNetworkAddressesExpected +// pages := 0 +// err := servers.ListAddressesByNetwork(client.ServiceClient(), "asdfasdfasdf", "public").EachPage(func(page pagination.Page) (bool, error) { +// pages++ + +// actual, err := servers.ExtractNetworkAddresses(page) +// th.AssertNoErr(t, err) + +// if len(actual) != 2 { +// t.Fatalf("Expected 2 addresses, got %d", len(actual)) +// } +// th.CheckDeepEquals(t, expected, actual) + +// return true, nil +// }) +// th.AssertNoErr(t, err) +// th.CheckEquals(t, 1, pages) +// } + +func TestCreateServerImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateServerImageSuccessfully(t) + + _, err := servers.CreateImage(client.ServiceClient(), "serverimage", servers.CreateImageOpts{Name: "test"}).ExtractImageID() + th.AssertNoErr(t, err) +} + +func TestMarshalPersonality(t *testing.T) { + name := "/etc/test" + contents := []byte("asdfasdf") + + personality := servers.Personality{ + &servers.File{ + Path: name, + Contents: contents, + }, + } + + data, err := json.Marshal(personality) + if err != nil { + t.Fatal(err) + } + + var actual []map[string]string + err = json.Unmarshal(data, &actual) + if err != nil { + t.Fatal(err) + } + + if len(actual) != 1 { + t.Fatal("expected personality length 1") + } + + if actual[0]["path"] != name { + t.Fatal("file path incorrect") + } + + if actual[0]["contents"] != base64.StdEncoding.EncodeToString(contents) { + t.Fatal("file contents incorrect") + } +} diff --git a/openstack/compute/v2/servers/testing/results_test.go b/openstack/compute/v2/servers/testing/results_test.go new file mode 100644 index 000000000..2f387b721 --- /dev/null +++ b/openstack/compute/v2/servers/testing/results_test.go @@ -0,0 +1,110 @@ +package testing + +import ( + "crypto/rsa" + "encoding/json" + "fmt" + "testing" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/compute/v2/servers" + th "github.com/huaweicloud/golangsdk/testhelper" + // "github.com/huaweicloud/golangsdk/testhelper/client" + "golang.org/x/crypto/ssh" +) + +// Fail - No password in JSON. +func TestExtractPassword_no_pwd_data(t *testing.T) { + + var dejson interface{} + err := json.Unmarshal([]byte(`{ "Crappy data": ".-.-." }`), &dejson) + if err != nil { + t.Fatalf("%s", err) + } + resp := servers.GetPasswordResult{Result: golangsdk.Result{Body: dejson}} + + pwd, err := resp.ExtractPassword(nil) + th.AssertEquals(t, pwd, "") +} + +// Ok - return encrypted password when no private key is given. +func TestExtractPassword_encrypted_pwd(t *testing.T) { + + var dejson interface{} + sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`) + + err := json.Unmarshal(sejson, &dejson) + fmt.Printf("%v\n", dejson) + if err != nil { + t.Fatalf("%s", err) + } + resp := servers.GetPasswordResult{Result: golangsdk.Result{Body: dejson}} + + pwd, err := resp.ExtractPassword(nil) + th.AssertNoErr(t, err) + th.AssertEquals(t, "PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw==", pwd) +} + +// Ok - return decrypted password when private key is given. +// Decrytion can be verified by: +// echo "" | base64 -D | openssl rsautl -decrypt -inkey +func TestExtractPassword_decrypted_pwd(t *testing.T) { + + privateKey, err := ssh.ParseRawPrivateKey([]byte(` +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAo1ODZgwMVdTJYim9UYuYhowoPMhGEuV5IRZjcJ315r7RBSC+ +yEiBb1V+jhf+P8fzAyU35lkBzZGDr7E3jxSesbOuYT8cItQS4ErUnI1LGuqvMxwv +X3GMyE/HmOcaiODF1XZN3Ur5pMJdVknnmczgUsW0hT98Udrh3MQn9WSuh/6LRy6+ +x1QsKHOCLFPnkhWa3LKyxmpQq/Gvhz+6NLe+gt8MFullA5mKQxBJ/K6laVHeaMlw +JG3GCX0EZhRlvzoV8koIBKZtbKFolFr8ZtxBm3R5LvnyrtOvp22sa+xeItUT5kG1 +ZnbGNdK87oYW+VigEUfzT/+8R1i6E2QIXoeZiQIDAQABAoIBAQCVZ70IqbbTAW8j +RAlyQh/J3Qal65LmkFJJKUDX8TfT1/Q/G6BKeMEmxm+Zrmsfj1pHI1HKftt+YEG1 +g4jOc09kQXkgbmnfll6aHPn3J+1vdwXD3GGdjrL5PrnYrngAhJWU2r8J0x8hT8ew +OrUJZXhDX6XuSpAAFRmOKUZgXbSmo4X+LZX76ACnarselJt5FL724ECvpWJ7xxC4 +FMzvp4RqMmNFvv/Uq9lE/EmoSk4dviYyIZZ16DbDNyc9k/sGqCAMktCEwZ3EQm// +S5bkNhgP6oUXjluWy53aPRgykEylgDWo5SSdSEyKnw/fciU0xdprA9JrBGIcTyHS +/k2kgD4xAoGBANTkJ88Q0YrxX3fZNZVqcn00XKTxPGmxN5LRs7eV743q30AxK5Db +QU8iwaAA1IKUWV5DLhgUTNsDCOPUPue4aOSBD3/sj+WEmvIhj7afDL5didkYHsqf +fDnhFHq7y/3i57d428C7BwwR79pGWVyi7vH3pfu9A1iwl1aNOae+zvbVAoGBAMRm +AmwQ9fJ3Qc44jysFK/yliLRGdShjkMMah5G3JlrelwfPtwPwEL2EHHhJB/C1acMs +n6Q6RaoF6WNSZUY65ksQg7aPOYf2X0FTFwQJvwDJ4qlWjmq7w+tQ0AoGJG+dVUmQ +zHZ/Y+HokSXzz9c4oevk4v/rMgAQ00WHrTdtIhnlAoGBALIJJ72D7CkNGHCq5qPQ +xHQukPejgolFGhufYXM7YX3GmPMe67cVlTVv9Isxhoa5N0+cUPT0LR3PGOUm/4Bb +eOT3hZXOqLwhvE6XgI8Rzd95bClwgXekDoh80dqeKMdmta961BQGlKskaPiacmsF +G1yhZV70P9Mwwy8vpbLB4GUNAoGAbTwbjsWkNfa0qCF3J8NZoszjCvnBQfSW2J1R +1+8ZKyNwt0yFi3Ajr3TibNiZzPzp1T9lj29FvfpJxA9Y+sXZvthxmcFxizix5GB1 +ha5yCNtA8VSOI7lJkAFDpL+j1lyYyjD6N9JE2KqEyKoh6J+8F7sXsqW7CqRRDfQX +mKNfey0CgYEAxcEoNoADN2hRl7qY9rbQfVvQb3RkoQkdHhl9gpLFCcV32IP8R4xg +09NbQK5OmgcIuZhLVNzTmUHJbabEGeXqIFIV0DsqECAt3WzbDyKQO23VJysFD46c +KSde3I0ybDz7iS2EtceKB7m4C0slYd+oBkm4efuF00rCOKDwpFq45m0= +-----END RSA PRIVATE KEY----- +`)) + if err != nil { + t.Fatalf("Error parsing private key: %s\n", err) + } + + var dejson interface{} + sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`) + + err = json.Unmarshal(sejson, &dejson) + fmt.Printf("%v\n", dejson) + if err != nil { + t.Fatalf("%s", err) + } + resp := servers.GetPasswordResult{Result: golangsdk.Result{Body: dejson}} + + pwd, err := resp.ExtractPassword(privateKey.(*rsa.PrivateKey)) + th.AssertNoErr(t, err) + th.AssertEquals(t, "ruZKK0tqxRfYm5t7lSJq", pwd) +} + +// func TestListAddressesAllPages(t *testing.T) { +// th.SetupHTTP() +// defer th.TeardownHTTP() +// HandleAddressListSuccessfully(t) + +// allPages, err := servers.ListAddresses(client.ServiceClient(), "asdfasdfasdf").AllPages() +// th.AssertNoErr(t, err) +// _, err = servers.ExtractAddresses(allPages) +// th.AssertNoErr(t, err) +// } diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go new file mode 100644 index 000000000..5c6894053 --- /dev/null +++ b/openstack/compute/v2/servers/urls.go @@ -0,0 +1,51 @@ +package servers + +import "github.com/huaweicloud/golangsdk" + +func createURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *golangsdk.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *golangsdk.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *golangsdk.ServiceClient, id string) string { + return deleteURL(client, id) +} + +// func updateURL(client *golangsdk.ServiceClient, id string) string { +// return deleteURL(client, id) +// } + +func actionURL(client *golangsdk.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +func metadatumURL(client *golangsdk.ServiceClient, id, key string) string { + return client.ServiceURL("servers", id, "metadata", key) +} + +func metadataURL(client *golangsdk.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "metadata") +} + +// func listAddressesURL(client *golangsdk.ServiceClient, id string) string { +// return client.ServiceURL("servers", id, "ips") +// } + +// func listAddressesByNetworkURL(client *golangsdk.ServiceClient, id, network string) string { +// return client.ServiceURL("servers", id, "ips", network) +// } + +// func passwordURL(client *golangsdk.ServiceClient, id string) string { +// return client.ServiceURL("servers", id, "os-server-password") +// } diff --git a/openstack/compute/v2/servers/util.go b/openstack/compute/v2/servers/util.go new file mode 100644 index 000000000..e16e25172 --- /dev/null +++ b/openstack/compute/v2/servers/util.go @@ -0,0 +1,21 @@ +package servers + +import "github.com/huaweicloud/golangsdk" + +// WaitForStatus will continually poll a server until it successfully +// transitions to a specified status. It will do this for at most the number +// of seconds specified. +func WaitForStatus(c *golangsdk.ServiceClient, id, status string, secs int) error { + return golangsdk.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +}