diff --git a/cmd/drone-autoscaler/main.go b/cmd/drone-autoscaler/main.go index 4a7706de..031dfba9 100644 --- a/cmd/drone-autoscaler/main.go +++ b/cmd/drone-autoscaler/main.go @@ -315,6 +315,7 @@ func setupProvider(c config.Config) (autoscaler.Provider, error) { openstack.WithImage(c.OpenStack.Image), openstack.WithRegion(c.OpenStack.Region), openstack.WithFlavor(c.OpenStack.Flavor), + openstack.WithNetwork(c.OpenStack.Network), openstack.WithFloatingIpPool(c.OpenStack.Pool), openstack.WithSSHKey(c.OpenStack.SSHKey), openstack.WithSecurityGroup(c.OpenStack.SecurityGroup...), diff --git a/config/config.go b/config/config.go index f3586cb8..bc0417fc 100644 --- a/config/config.go +++ b/config/config.go @@ -192,6 +192,7 @@ type ( Region string `envconfig:"OS_REGION_NAME"` Image string Flavor string + Network string Pool string `envconfig:"DRONE_OPENSTACK_IP_POOL"` SecurityGroup []string `split_words:"true"` SSHKey string diff --git a/config/load_test.go b/config/load_test.go index 24894c9b..98dddfa3 100644 --- a/config/load_test.go +++ b/config/load_test.go @@ -143,6 +143,7 @@ func TestLoad(t *testing.T) { "DRONE_PACKET_USERDATA_FILE": "/path/to/cloud/init.yml", "DRONE_PACKET_HOSTNAME": "agent", "DRONE_PACKET_TAGS": "drone,agent,prod", + "DRONE_OPENSTACK_NETWORK": "my-subnet-1", "DRONE_OPENSTACK_IP_POOL": "ext-ips-1", "DRONE_OPENSTACK_SSHKEY": "drone-ci", "DRONE_OPENSTACK_SECURITY_GROUP": "secgrp-feedface", @@ -321,6 +322,7 @@ var jsonConfig = []byte(`{ "Region": "sto-01", "Image": "ubuntu-16.04-server-latest", "Flavor": "t1.medium", + "Network": "my-subnet-1", "Pool": "ext-ips-1", "SecurityGroup": [ "secgrp-feedface" diff --git a/drivers/openstack/create.go b/drivers/openstack/create.go index cf0e46d3..8e2b75b6 100644 --- a/drivers/openstack/create.go +++ b/drivers/openstack/create.go @@ -10,10 +10,11 @@ import ( "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" - "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/pagination" ) // Create creates an OpenStack instance @@ -27,18 +28,37 @@ func (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpt if err != nil { return nil, err } - // Make a floating ip to attach. - ip, err := floatingips.Create(p.computeClient, floatingips.CreateOpts{ - Pool: p.pool, - }).Extract() - if err != nil { - return nil, err + + logger := logger.FromContext(ctx). + WithField("region", p.region). + WithField("image", p.image). + WithField("flavor", p.flavor). + WithField("network", p.network). + WithField("pool", p.pool). + WithField("name", opts.Name) + + logger.Debugln("instance create") + + nets := make([]servers.Network, 0) + + if p.network != "" { + network, err := networks.Get(p.networkClient, p.network).Extract() + if err != nil { + logger.WithError(err). + Debugln("failed to find network") + return nil, err + } + + nets = append(nets, servers.Network{ + UUID: network.ID, + }) } serverCreateOpts := servers.CreateOpts{ Name: opts.Name, - ImageName: p.image, - FlavorName: p.flavor, + ImageRef: p.image, + FlavorRef: p.flavor, + Networks: nets, UserData: buf.Bytes(), ServiceClient: p.computeClient, Metadata: p.metadata, @@ -50,35 +70,80 @@ func (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpt } server, err := servers.Create(p.computeClient, createOpts).Extract() if err != nil { - floatingips.Delete(p.computeClient, ip.ID) + logger.WithError(err). + Debugln("failed to create server") return nil, err } - logger := logger.FromContext(ctx). - WithField("region", p.region). - WithField("image", p.image). - WithField("sizes", p.flavor). - WithField("name", opts.Name) err = servers.WaitForStatus(p.computeClient, server.ID, "ACTIVE", 300) if err != nil { + logger.WithError(err). + Debugln("failed waiting for server") return nil, err } - floatingips.AssociateInstance(p.computeClient, server.ID, floatingips.AssociateOpts{ - FloatingIP: ip.IP, - }) - - logger.Debugln("instance create") instance := &autoscaler.Instance{ Provider: autoscaler.ProviderOpenStack, ID: server.ID, Name: server.Name, Region: p.region, - Address: ip.IP, Image: p.image, Size: p.flavor, } + if p.network != "" { + network, err := networks.Get(p.networkClient, p.network).Extract() + if err != nil { + logger.WithError(err). + Debugln("failed to find network") + return nil, err + } + + if err := servers.ListAddresses(p.computeClient, server.ID).EachPage(func(page pagination.Page) (bool, error) { + result, err := servers.ExtractAddresses(page) + if err != nil { + return false, err + } + + for name, addresses := range result { + if name == network.Name { + for _, address := range addresses { + instance.Address = address.Address + return true, nil + } + } + + } + + return false, nil + }); err != nil { + logger.WithError(err). + Debugln("failed to fetch address") + return nil, err + } + } + + if p.pool != "" { + ip, err := floatingips.Create(p.computeClient, floatingips.CreateOpts{ + Pool: p.pool, + }).Extract() + if err != nil { + logger.WithError(err). + Debugln("failed to create floating ip") + return nil, err + } + + if err := floatingips.AssociateInstance(p.computeClient, server.ID, floatingips.AssociateOpts{ + FloatingIP: ip.IP, + }).ExtractErr(); err != nil { + logger.WithError(err). + Debugln("failed to associate floating ip") + return nil, err + } + + instance.Address = ip.IP + } + logger. WithField("name", instance.Name). WithField("ip", instance.Address). diff --git a/drivers/openstack/create_test.go b/drivers/openstack/create_test.go index 7227048c..383db5e4 100644 --- a/drivers/openstack/create_test.go +++ b/drivers/openstack/create_test.go @@ -6,10 +6,11 @@ package openstack import ( "context" - "github.com/drone/autoscaler" - "github.com/h2non/gock" "os" "testing" + + "github.com/drone/autoscaler" + "github.com/h2non/gock" ) func TestCreate(t *testing.T) { @@ -31,6 +32,21 @@ func TestCreate(t *testing.T) { SetHeader("X-Subject-Token", authToken). BodyString(string(tokenResp1)) + authResp2 := helperLoad(t, "authresp1.json") + gock.New("http://ops.my.cloud"). + Get("/identity"). + Reply(300). + SetHeader("Content-Type", "application/json"). + BodyString(string(authResp2)) + + tokenResp2 := helperLoad(t, "tokenresp1.json") + gock.New("http://ops.my.cloud"). + Post("/identity/v3/auth/tokens"). + Reply(201). + SetHeader("Content-Type", "application/json"). + SetHeader("X-Subject-Token", authToken). + BodyString(string(tokenResp2)) + fipResp1 := helperLoad(t, "fipresp1.json") gock.New("http://ops.my.cloud"). Post("/compute/v2.1/os-floating-ips"). @@ -113,6 +129,7 @@ func TestAuthFail(t *testing.T) { if err != nil { t.Error("Unable to set OS_PASSWORD") } + authResp1 := helperLoad(t, "authresp1.json") gock.New("http://ops.my.cloud"). Get("/identity"). @@ -160,13 +177,20 @@ func TestCreateFail(t *testing.T) { SetHeader("X-Subject-Token", authToken). BodyString(string(tokenResp1)) - fipResp1 := helperLoad(t, "fipresp1.json") + authResp2 := helperLoad(t, "authresp1.json") gock.New("http://ops.my.cloud"). - Post("/compute/v2.1/os-floating-ips"). - MatchHeader("X-Auth-Token", authToken). - Reply(200). + Get("/identity"). + Reply(300). SetHeader("Content-Type", "application/json"). - BodyString(string(fipResp1)) + BodyString(string(authResp2)) + + tokenResp2 := helperLoad(t, "tokenresp1.json") + gock.New("http://ops.my.cloud"). + Post("/identity/v3/auth/tokens"). + Reply(201). + SetHeader("Content-Type", "application/json"). + SetHeader("X-Subject-Token", authToken). + BodyString(string(tokenResp2)) imageListResp := helperLoad(t, "imagelistresp1.json") gock.New("http://ops.my.cloud"). @@ -240,7 +264,7 @@ func testInstance(instance *autoscaler.Instance) func(t *testing.T) { if want, got := instance.Address, "172.24.4.5"; got != want { t.Errorf("Want instance IP %q, got %q", want, got) } - if want, got := instance.Image, "ubuntu-16.04-server-latest"; got != want { + if want, got := instance.Image, "4ef19958-ee2d-44a7-a100-de0b8afdbc8e"; got != want { t.Errorf("Want instance ID %q, got %q", want, got) } if want, got := instance.ID, "56046f6d-3184-495b-938b-baa450db970d"; got != want { @@ -255,7 +279,7 @@ func testInstance(instance *autoscaler.Instance) func(t *testing.T) { if want, got := instance.Region, "RegionOne"; got != want { t.Errorf("Want instance Region %q, got %q", want, got) } - if want, got := instance.Size, "m1.small"; got != want { + if want, got := instance.Size, "29e3cce3-d771-4220-80fe-3edf0e8dd466"; got != want { t.Errorf("Want instance Size %q, got %q", want, got) } } diff --git a/drivers/openstack/destroy.go b/drivers/openstack/destroy.go index c9831410..62e5f30f 100644 --- a/drivers/openstack/destroy.go +++ b/drivers/openstack/destroy.go @@ -6,41 +6,60 @@ package openstack import ( "context" + "fmt" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/pagination" ) func (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error { logger := logger.FromContext(ctx). WithField("region", instance.Region). WithField("image", instance.Image). - WithField("size", instance.Size). + WithField("flavor", instance.Size). WithField("name", instance.Name) logger.Debugln("deleting instance") - _ = p.deleteFloatingIps(instance) + err := p.deleteFloatingIps(instance) + if err != nil { + logger.WithError(err). + Debugln("failed to delete floating ips") - err := servers.Delete(p.computeClient, instance.ID).ExtractErr() + return err + } + + err = servers.Delete(p.computeClient, instance.ID).ExtractErr() if err == nil { logger.Debugln("instance deleted") return nil } + if err.Error() == "Resource not found" { + logger.WithError(err). + Debugln("instance does not exist") + return autoscaler.ErrInstanceNotFound + } + logger.WithError(err). - Errorln("deleting instance failed, attempting to force") + Errorln("attempting to force delete") err = servers.ForceDelete(p.computeClient, instance.ID).ExtractErr() - if err == nil { logger.Debugln("instance deleted") return nil } + if err.Error() == "Resource not found" { + logger.WithError(err). + Debugln("instance does not exist") + return autoscaler.ErrInstanceNotFound + } + logger.WithError(err). Errorln("force-deleting instance failed") @@ -48,20 +67,26 @@ func (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) e } func (p *provider) deleteFloatingIps(instance *autoscaler.Instance) error { - floatingips.DisassociateInstance(p.computeClient, instance.ID, floatingips.DisassociateOpts{ - FloatingIP: instance.Address, - }) - // Remove our allocated ip from the pool. - allPages, err := floatingips.List(p.computeClient).AllPages() - ips, err := floatingips.ExtractFloatingIPs(allPages) - if err != nil { - return err - } - for _, fip := range ips { - if fip.InstanceID == instance.ID { - floatingips.Delete(p.computeClient, fip.ID) + return floatingips.List(p.computeClient).EachPage(func(page pagination.Page) (bool, error) { + ips, err := floatingips.ExtractFloatingIPs(page) + if err != nil { + return false, err } - } - return nil + for _, ip := range ips { + if ip.InstanceID == instance.ID { + if err := floatingips.DisassociateInstance(p.computeClient, instance.ID, floatingips.DisassociateOpts{ + FloatingIP: ip.IP, + }).ExtractErr(); err != nil { + return false, fmt.Errorf("failed to disassociate floating ip: %s", err) + } + + if err := floatingips.Delete(p.computeClient, ip.ID).ExtractErr(); err != nil { + return false, fmt.Errorf("failed to delete floating ip: %s", err) + } + } + } + + return true, nil + }) } diff --git a/drivers/openstack/destroy_test.go b/drivers/openstack/destroy_test.go index d16b4fb2..82636ad8 100644 --- a/drivers/openstack/destroy_test.go +++ b/drivers/openstack/destroy_test.go @@ -6,9 +6,10 @@ package openstack import ( "context" + "testing" + "github.com/drone/autoscaler" "github.com/h2non/gock" - "testing" ) func TestDestroy(t *testing.T) { @@ -30,10 +31,20 @@ func TestDestroy(t *testing.T) { SetHeader("X-Subject-Token", authToken). BodyString(string(tokenResp1)) + authResp2 := helperLoad(t, "authresp1.json") gock.New("http://ops.my.cloud"). - MatchHeader("X-Auth-Token", authToken). - Post("/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d/action"). - Reply(202) + Get("/identity"). + Reply(300). + SetHeader("Content-Type", "application/json"). + BodyString(string(authResp2)) + + tokenResp2 := helperLoad(t, "tokenresp1.json") + gock.New("http://ops.my.cloud"). + Post("/identity/v3/auth/tokens"). + Reply(201). + SetHeader("Content-Type", "application/json"). + SetHeader("X-Subject-Token", authToken). + BodyString(string(tokenResp2)) fipResp1 := helperLoad(t, "fipresp1.json") gock.New("http://ops.my.cloud"). @@ -48,6 +59,22 @@ func TestDestroy(t *testing.T) { Delete("/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d"). Reply(204) + imageListResp := helperLoad(t, "imagelistresp1.json") + gock.New("http://ops.my.cloud"). + Get("/compute/v2.1/images/detail"). + MatchHeader("X-Auth-Token", authToken). + Reply(200). + SetHeader("Content-Type", "application/json"). + BodyString(string(imageListResp)) + + flavorListResp1 := helperLoad(t, "flavorlistresp1.json") + gock.New("http://ops.my.cloud"). + Get("/compute/v2.1/flavors/detail"). + MatchHeader("X-Auth-Token", authToken). + Reply(200). + SetHeader("Content-Type", "application/json"). + BodyString(string(flavorListResp1)) + mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "56046f6d-3184-495b-938b-baa450db970d", diff --git a/drivers/openstack/option.go b/drivers/openstack/option.go index 297cbb97..800a5841 100644 --- a/drivers/openstack/option.go +++ b/drivers/openstack/option.go @@ -5,9 +5,10 @@ package openstack import ( + "io/ioutil" + "github.com/drone/autoscaler/drivers/internal/userdata" "github.com/gophercloud/gophercloud" - "io/ioutil" ) type Option func(*provider) @@ -39,6 +40,7 @@ func WithSecurityGroup(group ...string) Option { p.groups = group } } + // WithComputeClient returns an option to set the // GopherCloud ServiceClient. func WithComputeClient(computeClient *gophercloud.ServiceClient) Option { @@ -47,6 +49,14 @@ func WithComputeClient(computeClient *gophercloud.ServiceClient) Option { } } +// WithNetworkClient returns an option to set the +// GopherCloud ServiceClient. +func WithNetworkClient(networkClient *gophercloud.ServiceClient) Option { + return func(p *provider) { + p.networkClient = networkClient + } +} + // WithSSHKey returns an option to set the ssh key. func WithSSHKey(key string) Option { return func(p *provider) { @@ -54,10 +64,10 @@ func WithSSHKey(key string) Option { } } -// WithSubnet returns an option to set the subnet id. -func WithSubnet(id string) Option { +// WithNetwork returns an option to set the network id. +func WithNetwork(id string) Option { return func(p *provider) { - p.subnet = id + p.network = id } } diff --git a/drivers/openstack/option_test.go b/drivers/openstack/option_test.go index 73c68e2d..18a655c8 100644 --- a/drivers/openstack/option_test.go +++ b/drivers/openstack/option_test.go @@ -5,22 +5,24 @@ package openstack import ( - "github.com/gophercloud/gophercloud" "testing" + + "github.com/gophercloud/gophercloud" ) func TestOptions(t *testing.T) { v, err := New( WithComputeClient(&gophercloud.ServiceClient{}), + WithNetworkClient(&gophercloud.ServiceClient{}), WithFloatingIpPool("ext-ips-1"), - WithFlavor("t1.medium"), + WithFlavor("053dc448-045b-4c15-a4a0-1908b6b9310d"), WithSecurityGroup("drone-ci"), WithSSHKey("drone-ci"), WithRegion("sto-01"), - WithImage("ubuntu-16.04-server-latest"), + WithImage("0e9fe318-568f-417e-b2c1-f1218aa2712f"), WithMetadata(map[string]string{"foo": "bar", "baz": "qux"}), - WithSubnet("subnet-feedface"), - ) + WithNetwork("c7d172c8-96e6-40ab-aaaa-4a555e247c73"), + ) if err != nil { t.Error(err) return @@ -33,14 +35,14 @@ func TestOptions(t *testing.T) { if got, want := p.region, "sto-01"; got != want { t.Errorf("Want region %q, got %q", want, got) } - if got, want := p.flavor, "t1.medium"; got != want { + if got, want := p.flavor, "053dc448-045b-4c15-a4a0-1908b6b9310d"; got != want { t.Errorf("Want flavor %q, got %q", want, got) } - if got, want := p.image, "ubuntu-16.04-server-latest"; got != want { + if got, want := p.image, "0e9fe318-568f-417e-b2c1-f1218aa2712f"; got != want { t.Errorf("Want image %q, got %q", want, got) } - if got, want := p.subnet, "subnet-feedface"; got != want { - t.Errorf("Want subnet %q, got %q", want, got) + if got, want := p.network, "c7d172c8-96e6-40ab-aaaa-4a555e247c73"; got != want { + t.Errorf("Want network %q, got %q", want, got) } if got, want := p.key, "drone-ci"; got != want { t.Errorf("Want key %q, got %q", want, got) @@ -54,4 +56,4 @@ func TestOptions(t *testing.T) { if got, want := p.metadata["baz"], "qux"; got != want { t.Errorf("Want baz=%q metadata, got baz=%q", want, got) } -} \ No newline at end of file +} diff --git a/drivers/openstack/provider.go b/drivers/openstack/provider.go index ce8e3b08..7aaae273 100644 --- a/drivers/openstack/provider.go +++ b/drivers/openstack/provider.go @@ -5,6 +5,7 @@ package openstack import ( + "regexp" "sync" "text/template" @@ -12,6 +13,9 @@ import ( "github.com/drone/autoscaler/drivers/internal/userdata" "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/images" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" ) // provider implements an OpenStack provider @@ -22,13 +26,14 @@ type provider struct { region string image string flavor string - subnet string + network string pool string userdata *template.Template groups []string metadata map[string]string computeClient *gophercloud.ServiceClient + networkClient *gophercloud.ServiceClient } // New returns a new OpenStack provider. @@ -60,5 +65,54 @@ func New(opts ...Option) (autoscaler.Provider, error) { return nil, err } } + + if p.networkClient == nil { + authOpts, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + authClient, err := openstack.AuthenticatedClient(authOpts) + if err != nil { + return nil, err + } + + p.networkClient, err = openstack.NewNetworkV2(authClient, gophercloud.EndpointOpts{ + Region: p.region, + }) + if err != nil { + return nil, err + } + } + + if p.image != "" && !isUUID(p.image) { + uuid, err := images.IDFromName(p.computeClient, p.image) + if err != nil { + return nil, err + } + p.image = uuid + } + + if p.flavor != "" && !isUUID(p.flavor) { + uuid, err := flavors.IDFromName(p.computeClient, p.flavor) + if err != nil { + return nil, err + } + p.flavor = uuid + } + + if p.network != "" && !isUUID(p.network) { + uuid, err := networks.IDFromName(p.networkClient, p.network) + if err != nil { + return nil, err + } + p.network = uuid + } + return p, nil } + +func isUUID(uuid string) bool { + r := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$") + return r.MatchString(uuid) +} diff --git a/drivers/openstack/provider_test.go b/drivers/openstack/provider_test.go index d72abc54..4bcd1091 100644 --- a/drivers/openstack/provider_test.go +++ b/drivers/openstack/provider_test.go @@ -5,13 +5,15 @@ package openstack import ( - "github.com/gophercloud/gophercloud" "testing" + + "github.com/gophercloud/gophercloud" ) func TestDefaults(t *testing.T) { v, err := New( WithComputeClient(&gophercloud.ServiceClient{}), + WithNetworkClient(&gophercloud.ServiceClient{}), ) if err != nil { t.Error(err) @@ -20,4 +22,4 @@ func TestDefaults(t *testing.T) { p := v.(*provider) // Add tests if we set some actual defaults in the future. _ = p -} \ No newline at end of file +} diff --git a/drivers/openstack/testdata/flavorlistresp1.json b/drivers/openstack/testdata/flavorlistresp1.json index 57ed6063..338803c2 100644 --- a/drivers/openstack/testdata/flavorlistresp1.json +++ b/drivers/openstack/testdata/flavorlistresp1.json @@ -4,11 +4,11 @@ "name": "m1.tiny", "links": [ { - "href": "http://ops.my.cloud/compute/v2.1/flavors/1", + "href": "http://ops.my.cloud/compute/v2.1/flavors/20f8acd8-5660-45d2-a176-1dafe98d591f", "rel": "self" }, { - "href": "http://ops.my.cloud/compute/flavors/1", + "href": "http://ops.my.cloud/compute/flavors/20f8acd8-5660-45d2-a176-1dafe98d591f", "rel": "bookmark" } ], @@ -20,17 +20,17 @@ "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 1, - "id": "1" + "id": "20f8acd8-5660-45d2-a176-1dafe98d591f" }, { "name": "m1.small", "links": [ { - "href": "http://ops.my.cloud/compute/v2.1/flavors/2", + "href": "http://ops.my.cloud/compute/v2.1/flavors/29e3cce3-d771-4220-80fe-3edf0e8dd466", "rel": "self" }, { - "href": "http://ops.my.cloud/compute/flavors/2", + "href": "http://ops.my.cloud/compute/flavors/29e3cce3-d771-4220-80fe-3edf0e8dd466", "rel": "bookmark" } ], @@ -42,17 +42,17 @@ "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 20, - "id": "2" + "id": "29e3cce3-d771-4220-80fe-3edf0e8dd466" }, { "name": "m1.medium", "links": [ { - "href": "http://ops.my.cloud/compute/v2.1/flavors/3", + "href": "http://ops.my.cloud/compute/v2.1/flavors/2fe9e665-cf0f-4bff-a2a7-a0c19b15da7b", "rel": "self" }, { - "href": "http://ops.my.cloud/compute/flavors/3", + "href": "http://ops.my.cloud/compute/flavors/2fe9e665-cf0f-4bff-a2a7-a0c19b15da7b", "rel": "bookmark" } ], @@ -64,17 +64,17 @@ "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 40, - "id": "3" + "id": "2fe9e665-cf0f-4bff-a2a7-a0c19b15da7b" }, { "name": "m1.large", "links": [ { - "href": "http://ops.my.cloud/compute/v2.1/flavors/4", + "href": "http://ops.my.cloud/compute/v2.1/flavors/43832d64-56ed-401f-953c-d4d4c156f33a", "rel": "self" }, { - "href": "http://ops.my.cloud/compute/flavors/4", + "href": "http://ops.my.cloud/compute/flavors/43832d64-56ed-401f-953c-d4d4c156f33a", "rel": "bookmark" } ], @@ -86,17 +86,17 @@ "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 80, - "id": "4" + "id": "43832d64-56ed-401f-953c-d4d4c156f33a" }, { "name": "m1.xlarge", "links": [ { - "href": "http://ops.my.cloud/compute/v2.1/flavors/5", + "href": "http://ops.my.cloud/compute/v2.1/flavors/618945e9-beb4-4f20-88fd-e044a228156f", "rel": "self" }, { - "href": "http://ops.my.cloud/compute/flavors/5", + "href": "http://ops.my.cloud/compute/flavors/618945e9-beb4-4f20-88fd-e044a228156f", "rel": "bookmark" } ], @@ -108,17 +108,17 @@ "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 160, - "id": "5" + "id": "618945e9-beb4-4f20-88fd-e044a228156f" }, { "name": "cirros256", "links": [ { - "href": "http://ops.my.cloud/compute/v2.1/flavors/c1", + "href": "http://ops.my.cloud/compute/v2.1/flavors/67a446b2-53c3-460b-a2da-533ce9a0c527", "rel": "self" }, { - "href": "http://ops.my.cloud/compute/flavors/c1", + "href": "http://ops.my.cloud/compute/flavors/67a446b2-53c3-460b-a2da-533ce9a0c527", "rel": "bookmark" } ], @@ -130,17 +130,17 @@ "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 0, - "id": "c1" + "id": "67a446b2-53c3-460b-a2da-533ce9a0c527" }, { "name": "ds512M", "links": [ { - "href": "http://ops.my.cloud/compute/v2.1/flavors/d1", + "href": "http://ops.my.cloud/compute/v2.1/flavors/c7d172c8-96e6-40ab-aaaa-4a555e247c73", "rel": "self" }, { - "href": "http://ops.my.cloud/compute/flavors/d1", + "href": "http://ops.my.cloud/compute/flavors/c7d172c8-96e6-40ab-aaaa-4a555e247c73", "rel": "bookmark" } ], @@ -152,17 +152,17 @@ "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 5, - "id": "d1" + "id": "c7d172c8-96e6-40ab-aaaa-4a555e247c73" }, { "name": "ds1G", "links": [ { - "href": "http://ops.my.cloud/compute/v2.1/flavors/d2", + "href": "http://ops.my.cloud/compute/v2.1/flavors/c3a01655-1b6a-44aa-b095-cc28dd407e70", "rel": "self" }, { - "href": "http://ops.my.cloud/compute/flavors/d2", + "href": "http://ops.my.cloud/compute/flavors/c3a01655-1b6a-44aa-b095-cc28dd407e70", "rel": "bookmark" } ], @@ -174,17 +174,17 @@ "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 10, - "id": "d2" + "id": "c3a01655-1b6a-44aa-b095-cc28dd407e70" }, { "name": "ds2G", "links": [ { - "href": "http://ops.my.cloud/compute/v2.1/flavors/d3", + "href": "http://ops.my.cloud/compute/v2.1/flavors/053dc448-045b-4c15-a4a0-1908b6b9310d", "rel": "self" }, { - "href": "http://ops.my.cloud/compute/flavors/d3", + "href": "http://ops.my.cloud/compute/flavors/053dc448-045b-4c15-a4a0-1908b6b9310d", "rel": "bookmark" } ], @@ -196,17 +196,17 @@ "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 10, - "id": "d3" + "id": "053dc448-045b-4c15-a4a0-1908b6b9310d" }, { "name": "ds4G", "links": [ { - "href": "http://ops.my.cloud/compute/v2.1/flavors/d4", + "href": "http://ops.my.cloud/compute/v2.1/flavors/0e9fe318-568f-417e-b2c1-f1218aa2712f", "rel": "self" }, { - "href": "http://ops.my.cloud/compute/flavors/d4", + "href": "http://ops.my.cloud/compute/flavors/0e9fe318-568f-417e-b2c1-f1218aa2712f", "rel": "bookmark" } ], @@ -218,7 +218,7 @@ "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 20, - "id": "d4" + "id": "0e9fe318-568f-417e-b2c1-f1218aa2712f" } ] }