Making server action result types more consistent
diff --git a/_site/openstack/compute/v2/servers/data_test.go b/_site/openstack/compute/v2/servers/data_test.go
new file mode 100644
index 0000000..d3a0ee0
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/data_test.go
@@ -0,0 +1,328 @@
+package servers
+
+// Recorded responses for the server resource.
+
+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": {}
+ }
+ ]
+ }
+ `
+
+ 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": {}
+ }
+ }
+ `
+)
+
+var (
+ serverHerp = Server{
+ Status: "ACTIVE",
+ Updated: "2014-09-25T13:10:10Z",
+ 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: "2014-09-25T13:10:02Z",
+ TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+ Metadata: map[string]interface{}{},
+ }
+ serverDerp = Server{
+ Status: "ACTIVE",
+ Updated: "2014-09-25T13:04:49Z",
+ 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: "2014-09-25T13:04:41Z",
+ TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+ Metadata: map[string]interface{}{},
+ }
+)
diff --git a/_site/openstack/compute/v2/servers/doc.go b/_site/openstack/compute/v2/servers/doc.go
new file mode 100644
index 0000000..0a1791d
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/doc.go
@@ -0,0 +1,3 @@
+// Package servers provides convenient access to standard, OpenStack-defined
+// compute services.
+package servers
diff --git a/_site/openstack/compute/v2/servers/requests.go b/_site/openstack/compute/v2/servers/requests.go
new file mode 100644
index 0000000..1b6e3b8
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/requests.go
@@ -0,0 +1,452 @@
+package servers
+
+import (
+ "encoding/base64"
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List makes a request against the API to list servers accessible to you.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ServerPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ return pagination.NewPager(client, listDetailURL(client), createPage)
+}
+
+// CreateOptsBuilder describes struct types that can be accepted by the Create call.
+// The CreateOpts struct in this package does.
+type CreateOptsBuilder interface {
+ ToServerCreateMap() map[string]interface{}
+}
+
+// Network is used within CreateOpts to control a new server's network attachments.
+type Network struct {
+ // UUID of a nova-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 [optional] specifies a fixed IPv4 address to be used on this network.
+ FixedIP string
+}
+
+// CreateOpts specifies server creation parameters.
+type CreateOpts struct {
+ // Name [required] is the name to assign to the newly launched server.
+ Name string
+
+ // ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state.
+ // Optional if using the boot-from-volume extension.
+ ImageRef string
+
+ // FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs.
+ FlavorRef string
+
+ // SecurityGroups [optional] lists the names of the security groups to which this server should belong.
+ SecurityGroups []string
+
+ // UserData [optional] contains configuration information or scripts to use upon launch.
+ // Create will base64-encode it for you.
+ UserData []byte
+
+ // AvailabilityZone [optional] in which to launch the server.
+ AvailabilityZone string
+
+ // Networks [optional] 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
+
+ // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+ Metadata map[string]string
+
+ // Personality [optional] includes the path and contents of a file to inject into the server at launch.
+ // The maximum size of the file is 255 bytes (decoded).
+ Personality []byte
+
+ // ConfigDrive [optional] enables metadata injection through a configuration drive.
+ ConfigDrive bool
+}
+
+// ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToServerCreateMap() map[string]interface{} {
+ server := make(map[string]interface{})
+
+ server["name"] = opts.Name
+ server["imageRef"] = opts.ImageRef
+ server["flavorRef"] = opts.FlavorRef
+
+ if opts.UserData != nil {
+ encoded := base64.StdEncoding.EncodeToString(opts.UserData)
+ server["user_data"] = &encoded
+ }
+ if opts.Personality != nil {
+ encoded := base64.StdEncoding.EncodeToString(opts.Personality)
+ server["personality"] = &encoded
+ }
+ if opts.ConfigDrive {
+ server["config_drive"] = "true"
+ }
+ if opts.AvailabilityZone != "" {
+ server["availability_zone"] = opts.AvailabilityZone
+ }
+ if opts.Metadata != nil {
+ server["metadata"] = opts.Metadata
+ }
+
+ 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}
+ }
+ }
+ 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
+ }
+ }
+ }
+
+ return map[string]interface{}{"server": server}
+}
+
+// Create requests a server to be provisioned to the user in the current tenant.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+ Results: &result.Resp,
+ ReqBody: opts.ToServerCreateMap(),
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return result
+}
+
+// Delete requests that a server previously provisioned be removed from your account.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return err
+}
+
+// Get requests details on a single server, by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var result GetResult
+ _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+ Results: &result.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ })
+ return result
+}
+
+// UpdateOptsBuilder allows extentions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+ ToServerUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts struct {
+ // Name [optional] 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
+
+ // AccessIPv4 [optional] provides a new IPv4 address for the instance.
+ AccessIPv4 string
+
+ // AccessIPv6 [optional] provides a new IPv6 address for the instance.
+ AccessIPv6 string
+}
+
+// ToServerUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToServerUpdateMap() map[string]interface{} {
+ server := make(map[string]string)
+ if opts.Name != "" {
+ server["name"] = opts.Name
+ }
+ if opts.AccessIPv4 != "" {
+ server["accessIPv4"] = opts.AccessIPv4
+ }
+ if opts.AccessIPv6 != "" {
+ server["accessIPv6"] = opts.AccessIPv6
+ }
+ return map[string]interface{}{"server": server}
+}
+
+// Update requests that various attributes of the indicated server be changed.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+ var result UpdateResult
+ _, result.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
+ Results: &result.Resp,
+ ReqBody: opts.ToServerUpdateMap(),
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ })
+ return result
+}
+
+// ChangeAdminPassword alters the administrator or root password for a specified server.
+func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) ActionResult {
+ var req struct {
+ ChangePassword struct {
+ AdminPass string `json:"adminPass"`
+ } `json:"changePassword"`
+ }
+
+ req.ChangePassword.AdminPass = newPassword
+
+ var res ActionResult
+
+ _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: req,
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// ErrArgument errors occur when an argument supplied to a package function
+// fails to fall within acceptable values. For example, the Reboot() function
+// expects the "how" parameter to be one of HardReboot or SoftReboot. These
+// constants are (currently) strings, leading someone to wonder if they can pass
+// other string values instead, perhaps in an effort to break the API of their
+// provider. Reboot() returns this error in this situation.
+//
+// Function identifies which function was called/which function is generating
+// the error.
+// Argument identifies which formal argument was responsible for producing the
+// error.
+// Value provides the value as it was passed into the function.
+type ErrArgument struct {
+ Function, Argument string
+ Value interface{}
+}
+
+// Error yields a useful diagnostic for debugging purposes.
+func (e *ErrArgument) Error() string {
+ return fmt.Sprintf("Bad argument in call to %s, formal parameter %s, value %#v", e.Function, e.Argument, e.Value)
+}
+
+func (e *ErrArgument) String() string {
+ return e.Error()
+}
+
+// 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
+)
+
+// Reboot requests that a given server reboot.
+// Two methods exist for rebooting a server:
+//
+// HardReboot (aka PowerCycle) restarts 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 restored or the VM instance restarted.
+//
+// SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures.
+// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine.
+func Reboot(client *gophercloud.ServiceClient, id string, how RebootMethod) ActionResult {
+ var res ActionResult
+
+ if (how != SoftReboot) && (how != HardReboot) {
+ res.Err = &ErrArgument{
+ Function: "Reboot",
+ Argument: "how",
+ Value: how,
+ }
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: struct {
+ C map[string]string `json:"reboot"`
+ }{
+ map[string]string{"type": string(how)},
+ },
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// RebuildOptsBuilder is an interface that allows extensions to override the
+// default behaviour of rebuild options
+type RebuildOptsBuilder interface {
+ ToServerRebuildMap() (map[string]interface{}, error)
+}
+
+// RebuildOpts represents the configuration options used in a server rebuild
+// operation
+type RebuildOpts struct {
+ // Required. The ID of the image you want your server to be provisioned on
+ ImageID string
+
+ // Name to set the server to
+ Name string
+
+ // Required. The server's admin password
+ AdminPass string
+
+ // AccessIPv4 [optional] provides a new IPv4 address for the instance.
+ AccessIPv4 string
+
+ // AccessIPv6 [optional] provides a new IPv6 address for the instance.
+ AccessIPv6 string
+
+ // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+ Metadata map[string]string
+
+ // Personality [optional] includes the path and contents of a file to inject into the server at launch.
+ // The maximum size of the file is 255 bytes (decoded).
+ Personality []byte
+}
+
+// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON
+func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
+ var err error
+ server := make(map[string]interface{})
+
+ if opts.AdminPass == "" {
+ err = fmt.Errorf("AdminPass is required")
+ }
+
+ if opts.ImageID == "" {
+ err = fmt.Errorf("ImageID is required")
+ }
+
+ if err != nil {
+ return server, err
+ }
+
+ server["name"] = opts.Name
+ server["adminPass"] = opts.AdminPass
+ server["imageRef"] = opts.ImageID
+
+ if opts.AccessIPv4 != "" {
+ server["accessIPv4"] = opts.AccessIPv4
+ }
+
+ if opts.AccessIPv6 != "" {
+ server["accessIPv6"] = opts.AccessIPv6
+ }
+
+ if opts.Metadata != nil {
+ server["metadata"] = opts.Metadata
+ }
+
+ if opts.Personality != nil {
+ encoded := base64.StdEncoding.EncodeToString(opts.Personality)
+ server["personality"] = &encoded
+ }
+
+ return map[string]interface{}{"rebuild": server}, nil
+}
+
+// Rebuild will reprovision the server according to the configuration options
+// provided in the RebuildOpts struct.
+func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) RebuildResult {
+ var result RebuildResult
+
+ if id == "" {
+ result.Err = fmt.Errorf("ID is required")
+ return result
+ }
+
+ reqBody, err := opts.ToServerRebuildMap()
+ if err != nil {
+ result.Err = err
+ return result
+ }
+
+ _, result.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: &reqBody,
+ Results: &result.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return result
+}
+
+// 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 RESIZE_VERIFY 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 *gophercloud.ServiceClient, id, flavorRef string) ActionResult {
+ var res ActionResult
+
+ _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: struct {
+ R map[string]interface{} `json:"resize"`
+ }{
+ map[string]interface{}{"flavorRef": flavorRef},
+ },
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// ConfirmResize confirms a previous resize operation on a server.
+// See Resize() for more details.
+func ConfirmResize(client *gophercloud.ServiceClient, id string) ActionResult {
+ var res ActionResult
+
+ _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: map[string]interface{}{"confirmResize": nil},
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{204},
+ })
+
+ return res
+}
+
+// RevertResize cancels a previous resize operation on a server.
+// See Resize() for more details.
+func RevertResize(client *gophercloud.ServiceClient, id string) ActionResult {
+ var res ActionResult
+
+ _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: map[string]interface{}{"revertResize": nil},
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/_site/openstack/compute/v2/servers/requests_test.go b/_site/openstack/compute/v2/servers/requests_test.go
new file mode 100644
index 0000000..5089286
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/requests_test.go
@@ -0,0 +1,279 @@
+package servers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "bzbzbzbzbz"
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestListServers(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", 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)
+ }
+ })
+
+ client := serviceClient()
+ pages := 0
+ err := List(client).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractServers(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 2 {
+ t.Fatalf("Expected 2 servers, got %d", len(actual))
+ }
+ equalServers(t, serverHerp, actual[0])
+ equalServers(t, serverDerp, actual[1])
+
+ return true, nil
+ })
+
+ if err != nil {
+ t.Fatalf("Unexpected error from EachPage: %v", err)
+ }
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestCreateServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.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, singleServerBody)
+ })
+
+ client := serviceClient()
+ actual, err := Create(client, CreateOpts{
+ Name: "derp",
+ ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb",
+ FlavorRef: "1",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Create error: %v", err)
+ }
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestDeleteServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ err := Delete(client, "asdfasdfasdf")
+ if err != nil {
+ t.Fatalf("Unexpected Delete error: %v", err)
+ }
+}
+
+func TestGetServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, singleServerBody)
+ })
+
+ client := serviceClient()
+ actual, err := Get(client, "1234asdf").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Get error: %v", err)
+ }
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestUpdateServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PUT")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "Content-Type", "application/json")
+ testhelper.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
+
+ fmt.Fprintf(w, singleServerBody)
+ })
+
+ client := serviceClient()
+ actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Update error: %v", err)
+ }
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestChangeServerAdminPassword(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ res := ChangeAdminPassword(serviceClient(), "1234asdf", "new-password")
+ testhelper.AssertNoErr(t, res.Err)
+}
+
+func TestRebootServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ res := Reboot(serviceClient(), "1234asdf", SoftReboot)
+ testhelper.AssertNoErr(t, res.Err)
+}
+
+func TestRebuildServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.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, singleServerBody)
+ })
+
+ opts := 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 := Rebuild(serviceClient(), "1234asdf", opts).Extract()
+ testhelper.AssertNoErr(t, err)
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestResizeServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ res := Resize(serviceClient(), "1234asdf", "2")
+ testhelper.AssertNoErr(t, res.Err)
+}
+
+func TestConfirmResize(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "confirmResize": null }`)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := ConfirmResize(serviceClient(), "1234asdf")
+ testhelper.AssertNoErr(t, res.Err)
+}
+
+func TestRevertResize(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "revertResize": null }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ res := RevertResize(serviceClient(), "1234asdf")
+ testhelper.AssertNoErr(t, res.Err)
+}
diff --git a/_site/openstack/compute/v2/servers/results.go b/_site/openstack/compute/v2/servers/results.go
new file mode 100644
index 0000000..492c5a8
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/results.go
@@ -0,0 +1,141 @@
+package servers
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type serverResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets any serverResult as a Server, if possible.
+func (r serverResult) Extract() (*Server, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Server Server `mapstructure:"server"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &response)
+ return &response.Server, err
+}
+
+// CreateResult temporarily contains the response from a Create call.
+type CreateResult struct {
+ serverResult
+}
+
+// GetResult temporarily contains the response from a Get call.
+type GetResult struct {
+ serverResult
+}
+
+// UpdateResult temporarily contains the response from an Update call.
+type UpdateResult struct {
+ serverResult
+}
+
+// RebuildResult temporarily contains the response from a Rebuild call.
+type ActionResult struct {
+ serverResult
+}
+
+// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account.
+type Server struct {
+ // ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant.
+ ID string
+
+ // TenantID identifies the tenant owning this server resource.
+ TenantID string `mapstructure:"tenant_id"`
+
+ // UserID uniquely identifies the user account owning the tenant.
+ UserID string `mapstructure:"user_id"`
+
+ // Name contains the human-readable name for the server.
+ Name string
+
+ // Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created.
+ Updated string
+ Created string
+
+ HostID string
+
+ // Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE.
+ Status string
+
+ // Progress ranges from 0..100.
+ // A request made against the server completes only once Progress reaches 100.
+ Progress int
+
+ // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration.
+ AccessIPv4 string
+ AccessIPv6 string
+
+ // Image refers to a JSON object, which itself indicates the OS image used to deploy the server.
+ Image map[string]interface{}
+
+ // Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server.
+ Flavor map[string]interface{}
+
+ // Addresses includes a list of all IP addresses assigned to the server, keyed by pool.
+ Addresses map[string]interface{}
+
+ // Metadata includes a list of all user-specified key-value pairs attached to the server.
+ Metadata map[string]interface{}
+
+ // Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
+ Links []interface{}
+
+ // KeyName indicates which public key was injected into the server on launch.
+ KeyName string `mapstructure:"keyname"`
+
+ // 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 `mapstructure:"adminPass"`
+}
+
+// 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 (page ServerPage) IsEmpty() (bool, error) {
+ servers, err := ExtractServers(page)
+ if err != nil {
+ return true, err
+ }
+ return len(servers) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ServerPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"servers_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
+func ExtractServers(page pagination.Page) ([]Server, error) {
+ casted := page.(ServerPage).Body
+
+ var response struct {
+ Servers []Server `mapstructure:"servers"`
+ }
+ err := mapstructure.Decode(casted, &response)
+ return response.Servers, err
+}
diff --git a/_site/openstack/compute/v2/servers/servers_test.go b/_site/openstack/compute/v2/servers/servers_test.go
new file mode 100644
index 0000000..590fc8b
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/servers_test.go
@@ -0,0 +1,65 @@
+package servers
+
+import (
+ "reflect"
+ "testing"
+)
+
+// This provides more fine-grained failures when Servers differ, because Server structs are too damn big to compare by eye.
+// FIXME I should absolutely refactor this into a general-purpose thing in testhelper.
+func equalServers(t *testing.T, expected Server, actual Server) {
+ if expected.ID != actual.ID {
+ t.Errorf("ID differs. expected=[%s], actual=[%s]", expected.ID, actual.ID)
+ }
+ if expected.TenantID != actual.TenantID {
+ t.Errorf("TenantID differs. expected=[%s], actual=[%s]", expected.TenantID, actual.TenantID)
+ }
+ if expected.UserID != actual.UserID {
+ t.Errorf("UserID differs. expected=[%s], actual=[%s]", expected.UserID, actual.UserID)
+ }
+ if expected.Name != actual.Name {
+ t.Errorf("Name differs. expected=[%s], actual=[%s]", expected.Name, actual.Name)
+ }
+ if expected.Updated != actual.Updated {
+ t.Errorf("Updated differs. expected=[%s], actual=[%s]", expected.Updated, actual.Updated)
+ }
+ if expected.Created != actual.Created {
+ t.Errorf("Created differs. expected=[%s], actual=[%s]", expected.Created, actual.Created)
+ }
+ if expected.HostID != actual.HostID {
+ t.Errorf("HostID differs. expected=[%s], actual=[%s]", expected.HostID, actual.HostID)
+ }
+ if expected.Status != actual.Status {
+ t.Errorf("Status differs. expected=[%s], actual=[%s]", expected.Status, actual.Status)
+ }
+ if expected.Progress != actual.Progress {
+ t.Errorf("Progress differs. expected=[%s], actual=[%s]", expected.Progress, actual.Progress)
+ }
+ if expected.AccessIPv4 != actual.AccessIPv4 {
+ t.Errorf("AccessIPv4 differs. expected=[%s], actual=[%s]", expected.AccessIPv4, actual.AccessIPv4)
+ }
+ if expected.AccessIPv6 != actual.AccessIPv6 {
+ t.Errorf("AccessIPv6 differs. expected=[%s], actual=[%s]", expected.AccessIPv6, actual.AccessIPv6)
+ }
+ if !reflect.DeepEqual(expected.Image, actual.Image) {
+ t.Errorf("Image differs. expected=[%s], actual=[%s]", expected.Image, actual.Image)
+ }
+ if !reflect.DeepEqual(expected.Flavor, actual.Flavor) {
+ t.Errorf("Flavor differs. expected=[%s], actual=[%s]", expected.Flavor, actual.Flavor)
+ }
+ if !reflect.DeepEqual(expected.Addresses, actual.Addresses) {
+ t.Errorf("Addresses differ. expected=[%s], actual=[%s]", expected.Addresses, actual.Addresses)
+ }
+ if !reflect.DeepEqual(expected.Metadata, actual.Metadata) {
+ t.Errorf("Metadata differs. expected=[%s], actual=[%s]", expected.Metadata, actual.Metadata)
+ }
+ if !reflect.DeepEqual(expected.Links, actual.Links) {
+ t.Errorf("Links differs. expected=[%s], actual=[%s]", expected.Links, actual.Links)
+ }
+ if expected.KeyName != actual.KeyName {
+ t.Errorf("KeyName differs. expected=[%s], actual=[%s]", expected.KeyName, actual.KeyName)
+ }
+ if expected.AdminPass != actual.AdminPass {
+ t.Errorf("AdminPass differs. expected=[%s], actual=[%s]", expected.AdminPass, actual.AdminPass)
+ }
+}
diff --git a/_site/openstack/compute/v2/servers/urls.go b/_site/openstack/compute/v2/servers/urls.go
new file mode 100644
index 0000000..57587ab
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/urls.go
@@ -0,0 +1,31 @@
+package servers
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("servers")
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return createURL(client)
+}
+
+func listDetailURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("servers", "detail")
+}
+
+func deleteURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("servers", id)
+}
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+ return deleteURL(client, id)
+}
+
+func updateURL(client *gophercloud.ServiceClient, id string) string {
+ return deleteURL(client, id)
+}
+
+func actionURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("servers", id, "action")
+}
diff --git a/_site/openstack/compute/v2/servers/urls_test.go b/_site/openstack/compute/v2/servers/urls_test.go
new file mode 100644
index 0000000..cc895c9
--- /dev/null
+++ b/_site/openstack/compute/v2/servers/urls_test.go
@@ -0,0 +1,56 @@
+package servers
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "servers"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "servers"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListDetailURL(t *testing.T) {
+ actual := listDetailURL(endpointClient())
+ expected := endpoint + "servers/detail"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestActionURL(t *testing.T) {
+ actual := actionURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo/action"
+ th.CheckEquals(t, expected, actual)
+}