From 8357e1521aafcb1047c325e1b2955964ac00308d Mon Sep 17 00:00:00 2001 From: Thom May Date: Tue, 17 Feb 2015 10:48:48 +0000 Subject: [PATCH] Basic Rackspace cloud support This enables all but Load Balancer support for the Rackspace public cloud platform. --- Godeps/Godeps.json | 4 +- .../rackspace/gophercloud/.travis.yml | 3 + .../rackspace/gophercloud/CONTRIBUTING.md | 2 +- .../rackspace/gophercloud/UPGRADING.md | 4 +- .../blockstorage/v1/snapshots_test.go | 4 +- .../openstack/blockstorage/v1/volumes_test.go | 4 +- .../blockstorage/v1/volumetypes_test.go | 2 +- .../compute/v2/bootfromvolume_test.go | 11 +- .../openstack/compute/v2/compute_test.go | 2 +- .../openstack/compute/v2/keypairs_test.go | 74 +++ .../openstack/compute/v2/secdefrules_test.go | 72 +++ .../openstack/compute/v2/secgroup_test.go | 177 ++++++ .../openstack/compute/v2/servers_test.go | 57 ++ .../openstack/identity/v2/extension_test.go | 2 +- .../openstack/identity/v2/identity_test.go | 2 +- .../openstack/identity/v2/role_test.go | 58 ++ .../openstack/identity/v2/tenant_test.go | 2 +- .../openstack/identity/v2/token_test.go | 2 +- .../openstack/identity/v2/user_test.go | 127 ++++ .../openstack/networking/v2/port_test.go | 2 +- .../objectstorage/v1/accounts_test.go | 16 +- .../acceptance/rackspace/cdn/v1/base_test.go | 32 + .../acceptance/rackspace/cdn/v1/common.go | 23 + .../rackspace/cdn/v1/flavor_test.go | 47 ++ .../rackspace/cdn/v1/service_test.go | 93 +++ .../rackspace/cdn/v1/serviceasset_test.go | 32 + .../compute/v2/bootfromvolume_test.go | 9 +- .../rackspace/compute/v2/servers_test.go | 22 +- .../acceptance/rackspace/identity/v2/pkg.go | 1 + .../rackspace/identity/v2/role_test.go | 59 ++ .../rackspace/identity/v2/user_test.go | 93 +++ .../acceptance/rackspace/lb/v1/acl_test.go | 94 +++ .../acceptance/rackspace/lb/v1/common.go | 62 ++ .../acceptance/rackspace/lb/v1/lb_test.go | 214 +++++++ .../rackspace/lb/v1/monitor_test.go | 60 ++ .../acceptance/rackspace/lb/v1/node_test.go | 175 ++++++ .../rackspace/lb/v1/session_test.go | 47 ++ .../rackspace/lb/v1/throttle_test.go | 53 ++ .../acceptance/rackspace/lb/v1/vip_test.go | 83 +++ .../rackspace/networking/v2/common.go | 39 ++ .../rackspace/networking/v2/network_test.go | 65 ++ .../rackspace/networking/v2/port_test.go | 116 ++++ .../rackspace/networking/v2/subnet_test.go | 84 +++ .../objectstorage/v1/accounts_test.go | 17 +- .../objectstorage/v1/cdncontainers_test.go | 25 +- .../objectstorage/v1/containers_test.go | 27 +- .../objectstorage/v1/objects_test.go | 16 +- .../gophercloud/acceptance/tools/tools.go | 9 +- .../rackspace/gophercloud/auth_options.go | 26 +- .../rackspace/gophercloud/auth_results.go | 7 +- .../github.com/rackspace/gophercloud/doc.go | 67 ++ .../rackspace/gophercloud/endpoint_search.go | 77 ++- .../blockstorage/v1/snapshots/util_test.go | 38 -- .../blockstorage/v1/volumes/results.go | 2 +- .../blockstorage/v1/volumes/util_test.go | 38 -- .../gophercloud/openstack/cdn/v1/base/doc.go | 4 + .../openstack/cdn/v1/base/fixtures.go | 53 ++ .../openstack/cdn/v1/base/requests.go | 30 + .../openstack/cdn/v1/base/requests_test.go | 43 ++ .../openstack/cdn/v1/base/results.go | 35 ++ .../gophercloud/openstack/cdn/v1/base/urls.go | 11 + .../openstack/cdn/v1/flavors/doc.go | 6 + .../openstack/cdn/v1/flavors/fixtures.go | 82 +++ .../openstack/cdn/v1/flavors/requests.go | 27 + .../openstack/cdn/v1/flavors/requests_test.go | 90 +++ .../openstack/cdn/v1/flavors/results.go | 71 +++ .../openstack/cdn/v1/flavors/urls.go | 11 + .../openstack/cdn/v1/serviceassets/doc.go | 7 + .../cdn/v1/serviceassets/fixtures.go | 19 + .../cdn/v1/serviceassets/requests.go | 52 ++ .../cdn/v1/serviceassets/requests_test.go | 18 + .../openstack/cdn/v1/serviceassets/results.go | 8 + .../openstack/cdn/v1/serviceassets/urls.go | 7 + .../openstack/cdn/v1/services/doc.go | 7 + .../openstack/cdn/v1/services/errors.go | 7 + .../openstack/cdn/v1/services/fixtures.go | 372 +++++++++++ .../openstack/cdn/v1/services/requests.go | 391 ++++++++++++ .../cdn/v1/services/requests_test.go | 358 +++++++++++ .../openstack/cdn/v1/services/results.go | 316 ++++++++++ .../openstack/cdn/v1/services/urls.go | 23 + .../rackspace/gophercloud/openstack/client.go | 11 + .../compute/v2/extensions/defsecrules/doc.go | 1 + .../v2/extensions/defsecrules/fixtures.go | 108 ++++ .../v2/extensions/defsecrules/requests.go | 111 ++++ .../extensions/defsecrules/requests_test.go | 100 +++ .../v2/extensions/defsecrules/results.go | 69 +++ .../compute/v2/extensions/defsecrules/urls.go | 13 + .../v2/extensions/keypairs/requests.go | 27 +- .../compute/v2/extensions/secgroups/doc.go | 1 + .../v2/extensions/secgroups/fixtures.go | 265 ++++++++ .../v2/extensions/secgroups/requests.go | 298 +++++++++ .../v2/extensions/secgroups/requests_test.go | 248 ++++++++ .../v2/extensions/secgroups/results.go | 147 +++++ .../compute/v2/extensions/secgroups/urls.go | 32 + .../compute/v2/extensions/startstop/doc.go | 5 + .../v2/extensions/startstop/fixtures.go | 27 + .../v2/extensions/startstop/requests.go | 40 ++ .../v2/extensions/startstop/requests_test.go | 30 + .../openstack/compute/v2/servers/fixtures.go | 100 +++ .../openstack/compute/v2/servers/requests.go | 203 +++++- .../compute/v2/servers/requests_test.go | 90 +++ .../openstack/compute/v2/servers/results.go | 129 +++- .../openstack/compute/v2/servers/urls.go | 8 + .../openstack/compute/v2/servers/urls_test.go | 12 + .../openstack/compute/v2/servers/util_test.go | 38 -- .../v2/extensions/admin/roles/docs.go | 16 + .../v2/extensions/admin/roles/fixtures.go | 48 ++ .../v2/extensions/admin/roles/requests.go | 44 ++ .../extensions/admin/roles/requests_test.go | 64 ++ .../v2/extensions/admin/roles/results.go | 53 ++ .../v2/extensions/admin/roles/urls.go | 21 + .../openstack/identity/v2/users/doc.go | 1 + .../openstack/identity/v2/users/fixtures.go | 163 +++++ .../openstack/identity/v2/users/requests.go | 180 ++++++ .../identity/v2/users/requests_test.go | 165 +++++ .../openstack/identity/v2/users/results.go | 128 ++++ .../openstack/identity/v2/users/urls.go | 21 + .../identity/v3/services/requests_test.go | 2 +- .../v2/extensions/lbaas/members/requests.go | 2 +- .../networking/v2/networks/requests.go | 2 +- .../openstack/networking/v2/networks/urls.go | 4 + .../openstack/networking/v2/ports/requests.go | 1 - .../objectstorage/v1/accounts/fixtures.go | 8 +- .../v1/accounts/requests_test.go | 19 +- .../objectstorage/v1/accounts/results.go | 78 ++- .../objectstorage/v1/containers/fixtures.go | 1 + .../v1/containers/requests_test.go | 4 +- .../objectstorage/v1/containers/results.go | 131 ++++ .../objectstorage/v1/objects/fixtures.go | 24 +- .../objectstorage/v1/objects/requests.go | 78 ++- .../objectstorage/v1/objects/requests_test.go | 14 +- .../objectstorage/v1/objects/results.go | 284 ++++++++- .../rackspace/gophercloud/package.go | 38 -- .../rackspace/gophercloud/params.go | 142 ++++- .../rackspace/gophercloud/params_test.go | 17 +- .../rackspace/cdn/v1/base/delegate.go | 18 + .../rackspace/cdn/v1/base/delegate_test.go | 44 ++ .../gophercloud/rackspace/cdn/v1/base/doc.go | 4 + .../rackspace/cdn/v1/flavors/delegate.go | 18 + .../rackspace/cdn/v1/flavors/delegate_test.go | 90 +++ .../rackspace/cdn/v1/flavors/doc.go | 6 + .../cdn/v1/serviceassets/delegate.go | 13 + .../cdn/v1/serviceassets/delegate_test.go | 19 + .../rackspace/cdn/v1/serviceassets/doc.go | 7 + .../rackspace/cdn/v1/services/delegate.go | 37 ++ .../cdn/v1/services/delegate_test.go | 359 +++++++++++ .../rackspace/cdn/v1/services/doc.go | 7 + .../rackspace/gophercloud/rackspace/client.go | 33 + .../rackspace/compute/v2/flavors/fixtures.go | 1 + .../rackspace/compute/v2/images/fixtures.go | 1 + .../rackspace/compute/v2/servers/delegate.go | 5 + .../compute/v2/servers/delegate_test.go | 22 + .../rackspace/compute/v2/servers/fixtures.go | 135 ++++ .../rackspace/compute/v2/servers/requests.go | 5 + .../rackspace/identity/v2/roles/delegate.go | 53 ++ .../identity/v2/roles/delegate_test.go | 66 ++ .../rackspace/identity/v2/roles/fixtures.go | 49 ++ .../rackspace/identity/v2/users/delegate.go | 145 +++++ .../identity/v2/users/delegate_test.go | 111 ++++ .../rackspace/identity/v2/users/fixtures.go | 154 +++++ .../rackspace/identity/v2/users/results.go | 129 ++++ .../rackspace/identity/v2/users/urls.go | 7 + .../gophercloud/rackspace/lb/v1/acl/doc.go | 12 + .../rackspace/lb/v1/acl/fixtures.go | 109 ++++ .../rackspace/lb/v1/acl/requests.go | 127 ++++ .../rackspace/lb/v1/acl/requests_test.go | 91 +++ .../rackspace/lb/v1/acl/results.go | 72 +++ .../gophercloud/rackspace/lb/v1/acl/urls.go | 20 + .../gophercloud/rackspace/lb/v1/lbs/doc.go | 44 ++ .../rackspace/lb/v1/lbs/fixtures.go | 584 ++++++++++++++++++ .../rackspace/lb/v1/lbs/requests.go | 574 +++++++++++++++++ .../rackspace/lb/v1/lbs/requests_test.go | 438 +++++++++++++ .../rackspace/lb/v1/lbs/results.go | 420 +++++++++++++ .../gophercloud/rackspace/lb/v1/lbs/urls.go | 49 ++ .../rackspace/lb/v1/monitors/doc.go | 21 + .../rackspace/lb/v1/monitors/fixtures.go | 87 +++ .../rackspace/lb/v1/monitors/requests.go | 178 ++++++ .../rackspace/lb/v1/monitors/requests_test.go | 75 +++ .../rackspace/lb/v1/monitors/results.go | 90 +++ .../rackspace/lb/v1/monitors/urls.go | 16 + .../gophercloud/rackspace/lb/v1/nodes/doc.go | 35 ++ .../rackspace/lb/v1/nodes/fixtures.go | 207 +++++++ .../rackspace/lb/v1/nodes/requests.go | 279 +++++++++ .../rackspace/lb/v1/nodes/requests_test.go | 211 +++++++ .../rackspace/lb/v1/nodes/results.go | 210 +++++++ .../gophercloud/rackspace/lb/v1/nodes/urls.go | 25 + .../rackspace/lb/v1/sessions/doc.go | 30 + .../rackspace/lb/v1/sessions/fixtures.go | 58 ++ .../rackspace/lb/v1/sessions/requests.go | 82 +++ .../rackspace/lb/v1/sessions/requests_test.go | 44 ++ .../rackspace/lb/v1/sessions/results.go | 58 ++ .../rackspace/lb/v1/sessions/urls.go | 16 + .../gophercloud/rackspace/lb/v1/ssl/doc.go | 22 + .../rackspace/lb/v1/ssl/fixtures.go | 195 ++++++ .../rackspace/lb/v1/ssl/requests.go | 278 +++++++++ .../rackspace/lb/v1/ssl/requests_test.go | 167 +++++ .../rackspace/lb/v1/ssl/results.go | 148 +++++ .../gophercloud/rackspace/lb/v1/ssl/urls.go | 25 + .../rackspace/lb/v1/throttle/doc.go | 5 + .../rackspace/lb/v1/throttle/fixtures.go | 61 ++ .../rackspace/lb/v1/throttle/requests.go | 95 +++ .../rackspace/lb/v1/throttle/requests_test.go | 44 ++ .../rackspace/lb/v1/throttle/results.go | 43 ++ .../rackspace/lb/v1/throttle/urls.go | 16 + .../gophercloud/rackspace/lb/v1/vips/doc.go | 13 + .../rackspace/lb/v1/vips/fixtures.go | 88 +++ .../rackspace/lb/v1/vips/requests.go | 112 ++++ .../rackspace/lb/v1/vips/requests_test.go | 87 +++ .../rackspace/lb/v1/vips/results.go | 89 +++ .../gophercloud/rackspace/lb/v1/vips/urls.go | 20 + .../networking/v2/common/common_tests.go | 12 + .../networking/v2/networks/delegate.go | 41 ++ .../networking/v2/networks/delegate_test.go | 276 +++++++++ .../rackspace/networking/v2/ports/delegate.go | 40 ++ .../networking/v2/ports/delegate_test.go | 322 ++++++++++ .../networking/v2/subnets/delegate.go | 40 ++ .../networking/v2/subnets/delegate_test.go | 363 +++++++++++ .../v1/accounts/delegate_test.go | 10 +- .../v1/cdncontainers/delegate.go | 33 - .../v1/cdncontainers/requests.go | 99 +++ .../objectstorage/v1/cdncontainers/results.go | 145 ++++- .../objectstorage/v1/cdncontainers/urls.go | 8 + .../objectstorage/v1/objects/delegate.go | 4 + .../objectstorage/v1/objects/delegate_test.go | 14 +- .../rackspace/gophercloud/results.go | 107 +++- .../github.com/rackspace/gophercloud/util.go | 9 +- .../cloud-config/master-cloud-config.yaml | 69 +-- .../cloud-config/minion-cloud-config.yaml | 4 +- cluster/rackspace/util.sh | 7 +- pkg/cloudprovider/rackspace/MAINTAINERS.md | 3 + pkg/cloudprovider/rackspace/rackspace.go | 409 ++++++++++++ pkg/cloudprovider/rackspace/rackspace_test.go | 181 ++++++ pkg/controllermanager/plugins.go | 1 + pkg/master/server/plugins.go | 1 + 234 files changed, 17464 insertions(+), 435 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/keypairs_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secdefrules_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secgroup_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/role_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/user_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/base_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/common.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/flavor_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/service_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/serviceasset_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/pkg.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/role_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/user_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/acl_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/common.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/lb_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/monitor_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/node_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/session_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/throttle_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/vip_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/common.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/network_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/port_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/subnet_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/doc.go delete mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util_test.go delete mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/errors.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests_test.go delete mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/docs.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/urls.go delete mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/delegate.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/delegate_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/delegate.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/delegate_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/delegate.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/delegate_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/delegate.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/delegate_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/doc.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/fixtures.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/results.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/urls.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/common/common_tests.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate_test.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate.go create mode 100644 Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate_test.go create mode 100644 pkg/cloudprovider/rackspace/MAINTAINERS.md create mode 100644 pkg/cloudprovider/rackspace/rackspace.go create mode 100644 pkg/cloudprovider/rackspace/rackspace_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 29da48f9e2b..300c0abe86e 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -203,8 +203,8 @@ }, { "ImportPath": "github.com/rackspace/gophercloud", - "Comment": "v1.0.0", - "Rev": "da56de6a59e53fdd61be1b5d9b87df34c47ac420" + "Comment": "v1.0.0-336-g2a6e319", + "Rev": "2a6e3190447abe5d000f951595ead1cf98df72d8" }, { "ImportPath": "github.com/skynetservices/skydns/msg", diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml index cf4f8cafcc6..946f98cb3da 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml @@ -4,6 +4,8 @@ install: go: - 1.1 - 1.2 + - 1.3 + - 1.4 - tip script: script/cibuild after_success: @@ -12,3 +14,4 @@ after_success: - go get github.com/mattn/goveralls - export PATH=$PATH:$HOME/gopath/bin/ - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8 +sudo: false diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md index 4f596a1fe4a..93b798e5a2b 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md @@ -11,7 +11,7 @@ As a contributor you will need to setup your workspace in a slightly different way than just downloading it. Here are the basic installation instructions: 1. Configure your `$GOPATH` and run `go get` as described in the main -[README](/#how-to-install). +[README](/README.md#how-to-install). 2. Move into the directory that houses your local repository: diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md index da3758ba1cf..a702cfc5095 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md @@ -238,7 +238,7 @@ err := pager.EachPage(func(page pagination.Page) (bool, error) { imageList, err := images.ExtractImages(page) for _, i := range imageList { - // "i" will be a images.Image + // "i" will be an images.Image } }) ``` @@ -284,7 +284,7 @@ err := pager.EachPage(func(page pagination.Page) (bool, error) { imageList, err := images.ExtractImages(page) for _, i := range imageList { - // "i" will be a images.Image + // "i" will be an images.Image } }) ``` diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go index 9132ee5e702..7741aa98416 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go @@ -13,9 +13,9 @@ import ( func TestSnapshots(t *testing.T) { - client, err := newClient() + client, err := newClient(t) th.AssertNoErr(t, err) - + v, err := volumes.Create(client, &volumes.CreateOpts{ Name: "gophercloud-test-volume", Size: 1, diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go index 99da39a9736..7760427f08c 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go @@ -13,7 +13,7 @@ import ( th "github.com/rackspace/gophercloud/testhelper" ) -func newClient() (*gophercloud.ServiceClient, error) { +func newClient(t *testing.T) (*gophercloud.ServiceClient, error) { ao, err := openstack.AuthOptionsFromEnv() th.AssertNoErr(t, err) @@ -26,7 +26,7 @@ func newClient() (*gophercloud.ServiceClient, error) { } func TestVolumes(t *testing.T) { - client, err := newClient() + client, err := newClient(t) th.AssertNoErr(t, err) cv, err := volumes.Create(client, &volumes.CreateOpts{ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go index 5adcd819694..000bc01d575 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go @@ -12,7 +12,7 @@ import ( ) func TestVolumeTypes(t *testing.T) { - client, err := newClient() + client, err := newClient(t) th.AssertNoErr(t, err) vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go index d08abe6a543..add0e5fc11e 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go @@ -5,10 +5,10 @@ package v2 import ( "testing" + "github.com/rackspace/gophercloud/acceptance/tools" "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" "github.com/rackspace/gophercloud/openstack/compute/v2/servers" th "github.com/rackspace/gophercloud/testhelper" - "github.com/smashwilson/gophercloud/acceptance/tools" ) func TestBootFromVolume(t *testing.T) { @@ -37,14 +37,19 @@ func TestBootFromVolume(t *testing.T) { serverCreateOpts := servers.CreateOpts{ Name: name, - FlavorRef: "3", + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, } server, err := bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{ serverCreateOpts, bd, }).Extract() th.AssertNoErr(t, err) + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + t.Logf("Created server: %+v\n", server) - //defer deleteServer(t, client, server) + defer servers.Delete(client, server.ID) t.Logf("Deleting server [%s]...", name) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go index 46eb9ff46ec..33e49fea9bb 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go @@ -1,4 +1,4 @@ -// +build acceptance +// +build acceptance common package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/keypairs_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/keypairs_test.go new file mode 100644 index 00000000000..3e12d6b3cd4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/keypairs_test.go @@ -0,0 +1,74 @@ +// +build acceptance + +package v2 + +import ( + "crypto/rand" + "crypto/rsa" + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" + + "code.google.com/p/go.crypto/ssh" +) + +const keyName = "gophercloud_test_key_pair" + +func TestCreateServerWithKeyPair(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + publicKey := privateKey.PublicKey + pub, err := ssh.NewPublicKey(&publicKey) + th.AssertNoErr(t, err) + pubBytes := ssh.MarshalAuthorizedKey(pub) + pk := string(pubBytes) + + kp, err := keypairs.Create(client, keypairs.CreateOpts{ + Name: keyName, + PublicKey: pk, + }).Extract() + th.AssertNoErr(t, err) + t.Logf("Created key pair: %s\n", kp) + + choices, err := ComputeChoicesFromEnv() + th.AssertNoErr(t, err) + + name := tools.RandomString("Gophercloud-", 8) + t.Logf("Creating server [%s] with key pair.", name) + + serverCreateOpts := servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + } + + server, err := servers.Create(client, keypairs.CreateOptsExt{ + serverCreateOpts, + keyName, + }).Extract() + th.AssertNoErr(t, err) + defer servers.Delete(client, server.ID) + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + + server, err = servers.Get(client, server.ID).Extract() + t.Logf("Created server: %+v\n", server) + th.AssertNoErr(t, err) + th.AssertEquals(t, server.KeyName, keyName) + + t.Logf("Deleting key pair [%s]...", kp.Name) + err = keypairs.Delete(client, keyName).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Deleting server [%s]...", name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secdefrules_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secdefrules_test.go new file mode 100644 index 00000000000..78b07986bd2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secdefrules_test.go @@ -0,0 +1,72 @@ +// +build acceptance compute defsecrules + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + dsr "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSecDefRules(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + id := createDefRule(t, client) + + listDefRules(t, client) + + getDefRule(t, client, id) + + deleteDefRule(t, client, id) +} + +func createDefRule(t *testing.T, client *gophercloud.ServiceClient) string { + opts := dsr.CreateOpts{ + FromPort: tools.RandomInt(80, 89), + ToPort: tools.RandomInt(90, 99), + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + rule, err := dsr.Create(client, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created default rule %s", rule.ID) + + return rule.ID +} + +func listDefRules(t *testing.T, client *gophercloud.ServiceClient) { + err := dsr.List(client).EachPage(func(page pagination.Page) (bool, error) { + drList, err := dsr.ExtractDefaultRules(page) + th.AssertNoErr(t, err) + + for _, dr := range drList { + t.Logf("Listing default rule %s: Name [%s] From Port [%s] To Port [%s] Protocol [%s]", + dr.ID, dr.FromPort, dr.ToPort, dr.IPProtocol) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getDefRule(t *testing.T, client *gophercloud.ServiceClient, id string) { + rule, err := dsr.Get(client, id).Extract() + th.AssertNoErr(t, err) + + t.Logf("Getting rule %s: %#v", id, rule) +} + +func deleteDefRule(t *testing.T, client *gophercloud.ServiceClient, id string) { + err := dsr.Delete(client, id).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Deleted rule %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secgroup_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secgroup_test.go new file mode 100644 index 00000000000..4f507391093 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/secgroup_test.go @@ -0,0 +1,177 @@ +// +build acceptance compute secgroups + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSecGroups(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + serverID, needsDeletion := findServer(t, client) + + groupID := createSecGroup(t, client) + + listSecGroups(t, client) + + newName := tools.RandomString("secgroup_", 5) + updateSecGroup(t, client, groupID, newName) + + getSecGroup(t, client, groupID) + + addRemoveRules(t, client, groupID) + + addServerToSecGroup(t, client, serverID, newName) + + removeServerFromSecGroup(t, client, serverID, newName) + + if needsDeletion { + servers.Delete(client, serverID) + } + + deleteSecGroup(t, client, groupID) +} + +func createSecGroup(t *testing.T, client *gophercloud.ServiceClient) string { + opts := secgroups.CreateOpts{ + Name: tools.RandomString("secgroup_", 5), + Description: "something", + } + + group, err := secgroups.Create(client, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created secgroup %s %s", group.ID, group.Name) + + return group.ID +} + +func listSecGroups(t *testing.T, client *gophercloud.ServiceClient) { + err := secgroups.List(client).EachPage(func(page pagination.Page) (bool, error) { + secGrpList, err := secgroups.ExtractSecurityGroups(page) + th.AssertNoErr(t, err) + + for _, sg := range secGrpList { + t.Logf("Listing secgroup %s: Name [%s] Desc [%s] TenantID [%s]", sg.ID, + sg.Name, sg.Description, sg.TenantID) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updateSecGroup(t *testing.T, client *gophercloud.ServiceClient, id, newName string) { + opts := secgroups.UpdateOpts{ + Name: newName, + Description: tools.RandomString("dec_", 10), + } + group, err := secgroups.Update(client, id, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Updated %s's name to %s", group.ID, group.Name) +} + +func getSecGroup(t *testing.T, client *gophercloud.ServiceClient, id string) { + group, err := secgroups.Get(client, id).Extract() + th.AssertNoErr(t, err) + + t.Logf("Getting %s: %#v", id, group) +} + +func addRemoveRules(t *testing.T, client *gophercloud.ServiceClient, id string) { + opts := secgroups.CreateRuleOpts{ + ParentGroupID: id, + FromPort: 22, + ToPort: 22, + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + rule, err := secgroups.CreateRule(client, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Adding rule %s to group %s", rule.ID, id) + + err = secgroups.DeleteRule(client, rule.ID).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Deleted rule %s from group %s", rule.ID, id) +} + +func findServer(t *testing.T, client *gophercloud.ServiceClient) (string, bool) { + var serverID string + var needsDeletion bool + + err := servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + sList, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + + for _, s := range sList { + serverID = s.ID + needsDeletion = false + + t.Logf("Found an existing server: ID [%s]", serverID) + break + } + + return true, nil + }) + th.AssertNoErr(t, err) + + if serverID == "" { + t.Log("No server found, creating one") + + choices, err := ComputeChoicesFromEnv() + th.AssertNoErr(t, err) + + opts := &servers.CreateOpts{ + Name: tools.RandomString("secgroup_test_", 5), + ImageRef: choices.ImageID, + FlavorRef: choices.FlavorID, + } + + s, err := servers.Create(client, opts).Extract() + th.AssertNoErr(t, err) + serverID = s.ID + + t.Logf("Created server %s, waiting for it to build", s.ID) + err = servers.WaitForStatus(client, serverID, "ACTIVE", 300) + th.AssertNoErr(t, err) + + needsDeletion = true + } + + return serverID, needsDeletion +} + +func addServerToSecGroup(t *testing.T, client *gophercloud.ServiceClient, serverID, groupName string) { + err := secgroups.AddServerToGroup(client, serverID, groupName).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Adding group %s to server %s", groupName, serverID) +} + +func removeServerFromSecGroup(t *testing.T, client *gophercloud.ServiceClient, serverID, groupName string) { + err := secgroups.RemoveServerFromGroup(client, serverID, groupName).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Removing group %s from server %s", groupName, serverID) +} + +func deleteSecGroup(t *testing.T, client *gophercloud.ServiceClient, id string) { + err := secgroups.Delete(client, id).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Deleted group %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go index e223c18d16a..d52a9d35376 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go @@ -12,6 +12,7 @@ import ( "github.com/rackspace/gophercloud/openstack/compute/v2/servers" "github.com/rackspace/gophercloud/openstack/networking/v2/networks" "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" ) func TestListServers(t *testing.T) { @@ -94,6 +95,8 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *Comp name := tools.RandomString("ACPTTEST", 16) t.Logf("Attempting to create server: %s\n", name) + pwd := tools.MakeNewPassword("") + server, err := servers.Create(client, servers.CreateOpts{ Name: name, FlavorRef: choices.FlavorID, @@ -101,11 +104,14 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *Comp Networks: []servers.Network{ servers.Network{UUID: network.ID}, }, + AdminPass: pwd, }).Extract() if err != nil { t.Fatalf("Unable to create server: %v", err) } + th.AssertEquals(t, pwd, server.AdminPass) + return server, err } @@ -391,3 +397,54 @@ func TestActionResizeRevert(t *testing.T) { t.Fatal(err) } } + +func TestServerMetadata(t *testing.T) { + t.Parallel() + + choices, err := ComputeChoicesFromEnv() + th.AssertNoErr(t, err) + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := createServer(t, client, choices) + if err != nil { + t.Fatal(err) + } + defer servers.Delete(client, server.ID) + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + 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) + + err = servers.DeleteMetadatum(client, server.ID, "foo").ExtractErr() + th.AssertNoErr(t, err) + + metadata, err = servers.CreateMetadatum(client, server.ID, servers.MetadatumOpts{ + "foo": "baz", + }).Extract() + th.AssertNoErr(t, err) + t.Logf("CreateMetadatum result: %+v\n", 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) + + 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) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/extension_test.go index 2b4e062aef1..d1fa1e3dcec 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/extension_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/extension_test.go @@ -1,4 +1,4 @@ -// +build acceptance +// +build acceptance identity package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go index feae233b486..96bf1fdadea 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go @@ -1,4 +1,4 @@ -// +build acceptance +// +build acceptance identity package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/role_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/role_test.go new file mode 100644 index 00000000000..ba243fe02bf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/role_test.go @@ -0,0 +1,58 @@ +// +build acceptance identity roles + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestRoles(t *testing.T) { + client := authenticatedClient(t) + + tenantID := findTenant(t, client) + userID := createUser(t, client, tenantID) + roleID := listRoles(t, client) + + addUserRole(t, client, tenantID, userID, roleID) + + deleteUserRole(t, client, tenantID, userID, roleID) + + deleteUser(t, client, userID) +} + +func listRoles(t *testing.T, client *gophercloud.ServiceClient) string { + var roleID string + + err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) { + roleList, err := roles.ExtractRoles(page) + th.AssertNoErr(t, err) + + for _, role := range roleList { + t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name) + roleID = role.ID + } + + return true, nil + }) + + th.AssertNoErr(t, err) + + return roleID +} + +func addUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) { + err := roles.AddUserRole(client, tenantID, userID, roleID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Added role %s to user %s", roleID, userID) +} + +func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) { + err := roles.DeleteUserRole(client, tenantID, userID, roleID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Removed role %s from user %s", roleID, userID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/tenant_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/tenant_test.go index 2054598295a..578fc483b8e 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/tenant_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/tenant_test.go @@ -1,4 +1,4 @@ -// +build acceptance +// +build acceptance identity package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/token_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/token_test.go index 0632a48fc99..d9031408558 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/token_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/token_test.go @@ -1,4 +1,4 @@ -// +build acceptance +// +build acceptance identity package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/user_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/user_test.go new file mode 100644 index 00000000000..fe73d198987 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/user_test.go @@ -0,0 +1,127 @@ +// +build acceptance identity + +package v2 + +import ( + "strconv" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" + "github.com/rackspace/gophercloud/openstack/identity/v2/users" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestUsers(t *testing.T) { + client := authenticatedClient(t) + + tenantID := findTenant(t, client) + + userID := createUser(t, client, tenantID) + + listUsers(t, client) + + getUser(t, client, userID) + + updateUser(t, client, userID) + + listUserRoles(t, client, tenantID, userID) + + deleteUser(t, client, userID) +} + +func findTenant(t *testing.T, client *gophercloud.ServiceClient) string { + var tenantID string + err := tenants.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + tenantList, err := tenants.ExtractTenants(page) + th.AssertNoErr(t, err) + + for _, t := range tenantList { + tenantID = t.ID + break + } + + return true, nil + }) + th.AssertNoErr(t, err) + + return tenantID +} + +func createUser(t *testing.T, client *gophercloud.ServiceClient, tenantID string) string { + t.Log("Creating user") + + opts := users.CreateOpts{ + Name: tools.RandomString("user_", 5), + Enabled: users.Disabled, + TenantID: tenantID, + Email: "new_user@foo.com", + } + + user, err := users.Create(client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created user %s on tenant %s", user.ID, tenantID) + + return user.ID +} + +func listUsers(t *testing.T, client *gophercloud.ServiceClient) { + err := users.List(client).EachPage(func(page pagination.Page) (bool, error) { + userList, err := users.ExtractUsers(page) + th.AssertNoErr(t, err) + + for _, user := range userList { + t.Logf("Listing user: ID [%s] Name [%s] Email [%s] Enabled? [%s]", + user.ID, user.Name, user.Email, strconv.FormatBool(user.Enabled)) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + _, err := users.Get(client, userID).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting user %s", userID) +} + +func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + opts := users.UpdateOpts{Name: tools.RandomString("new_name", 5), Email: "new@foo.com"} + user, err := users.Update(client, userID, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Updated user %s: Name [%s] Email [%s]", userID, user.Name, user.Email) +} + +func listUserRoles(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID string) { + count := 0 + err := users.ListRoles(client, tenantID, userID).EachPage(func(page pagination.Page) (bool, error) { + count++ + + roleList, err := users.ExtractRoles(page) + th.AssertNoErr(t, err) + + t.Logf("Listing roles for user %s", userID) + + for _, r := range roleList { + t.Logf("- %s (%s)", r.Name, r.ID) + } + + return true, nil + }) + + if count == 0 { + t.Logf("No roles for user %s", userID) + } + + th.AssertNoErr(t, err) +} + +func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + res := users.Delete(client, userID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted user %s", userID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/port_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/port_test.go index 7f22dbd5cd6..03e8e278428 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/port_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/port_test.go @@ -82,7 +82,7 @@ func listPorts(t *testing.T) { th.AssertNoErr(t, err) for _, p := range portList { - t.Logf("Port: ID [%s] Name [%s] Status [%d] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]", + t.Logf("Port: ID [%s] Name [%s] Status [%s] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]", p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go index f7c01a7c118..24cc62b4aa0 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go @@ -17,7 +17,10 @@ func TestAccounts(t *testing.T) { // Update an account's metadata. updateres := accounts.Update(client, accounts.UpdateOpts{Metadata: metadata}) - th.AssertNoErr(t, updateres.Err) + t.Logf("Update Account Response: %+v\n", updateres) + updateHeaders, err := updateres.Extract() + th.AssertNoErr(t, err) + t.Logf("Update Account Response Headers: %+v\n", updateHeaders) // Defer the deletion of the metadata set above. defer func() { @@ -29,11 +32,14 @@ func TestAccounts(t *testing.T) { th.AssertNoErr(t, updateres.Err) }() - // Retrieve account metadata. - getres := accounts.Get(client, nil) - th.AssertNoErr(t, getres.Err) // Extract the custom metadata from the 'Get' response. - am, err := getres.ExtractMetadata() + res := accounts.Get(client, nil) + + h, err := res.Extract() + th.AssertNoErr(t, err) + t.Logf("Get Account Response Headers: %+v\n", h) + + am, err := res.ExtractMetadata() th.AssertNoErr(t, err) for k := range metadata { if am[k] != metadata[strings.Title(k)] { diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/base_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/base_test.go new file mode 100644 index 00000000000..135f5b330af --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/base_test.go @@ -0,0 +1,32 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/cdn/v1/base" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestBaseOps(t *testing.T) { + client := newClient(t) + t.Log("Retrieving Home Document") + testHomeDocumentGet(t, client) + + t.Log("Pinging root URL") + testPing(t, client) +} + +func testHomeDocumentGet(t *testing.T, client *gophercloud.ServiceClient) { + hd, err := base.Get(client).Extract() + th.AssertNoErr(t, err) + t.Logf("Retrieved home document: %+v", *hd) +} + +func testPing(t *testing.T, client *gophercloud.ServiceClient) { + err := base.Ping(client).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Successfully pinged root URL") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/common.go new file mode 100644 index 00000000000..2333ca77bff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/common.go @@ -0,0 +1,23 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newClient(t *testing.T) *gophercloud.ServiceClient { + ao, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := rackspace.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + c, err := rackspace.NewCDNV1(client, gophercloud.EndpointOpts{}) + th.AssertNoErr(t, err) + return c +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/flavor_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/flavor_test.go new file mode 100644 index 00000000000..f26cff0140c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/flavor_test.go @@ -0,0 +1,47 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestFlavor(t *testing.T) { + client := newClient(t) + + t.Log("Listing Flavors") + id := testFlavorsList(t, client) + + t.Log("Retrieving Flavor") + testFlavorGet(t, client, id) +} + +func testFlavorsList(t *testing.T, client *gophercloud.ServiceClient) string { + var id string + err := flavors.List(client).EachPage(func(page pagination.Page) (bool, error) { + flavorList, err := os.ExtractFlavors(page) + th.AssertNoErr(t, err) + + for _, flavor := range flavorList { + t.Logf("Listing flavor: ID [%s] Providers [%+v]", flavor.ID, flavor.Providers) + id = flavor.ID + } + + return true, nil + }) + + th.AssertNoErr(t, err) + return id +} + +func testFlavorGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + flavor, err := flavors.Get(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Retrieved Flavor: %+v", *flavor) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/service_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/service_test.go new file mode 100644 index 00000000000..c19c241c364 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/service_test.go @@ -0,0 +1,93 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/cdn/v1/services" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/cdn/v1/services" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestService(t *testing.T) { + client := newClient(t) + + t.Log("Creating Service") + loc := testServiceCreate(t, client, "test-site-1") + t.Logf("Created service at location: %s", loc) + + defer testServiceDelete(t, client, loc) + + t.Log("Updating Service") + testServiceUpdate(t, client, loc) + + t.Log("Retrieving Service") + testServiceGet(t, client, loc) + + t.Log("Listing Services") + testServiceList(t, client) +} + +func testServiceCreate(t *testing.T, client *gophercloud.ServiceClient, name string) string { + createOpts := os.CreateOpts{ + Name: name, + Domains: []os.Domain{ + os.Domain{ + Domain: "www." + name + ".com", + }, + }, + Origins: []os.Origin{ + os.Origin{ + Origin: name + ".com", + Port: 80, + SSL: false, + }, + }, + FlavorID: "cdn", + } + l, err := services.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + return l +} + +func testServiceGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + s, err := services.Get(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Retrieved service: %+v", *s) +} + +func testServiceUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) { + opts := os.UpdateOpts{ + os.Append{ + Value: os.Domain{Domain: "newDomain.com", Protocol: "http"}, + }, + } + + loc, err := services.Update(client, id, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Successfully updated service at location: %s", loc) +} + +func testServiceList(t *testing.T, client *gophercloud.ServiceClient) { + err := services.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + serviceList, err := os.ExtractServices(page) + th.AssertNoErr(t, err) + + for _, service := range serviceList { + t.Logf("Listing service: %+v", service) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func testServiceDelete(t *testing.T, client *gophercloud.ServiceClient, id string) { + err := services.Delete(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Successfully deleted service (%s)", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/serviceasset_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/serviceasset_test.go new file mode 100644 index 00000000000..c32bf253da2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/cdn/v1/serviceasset_test.go @@ -0,0 +1,32 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + osServiceAssets "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets" + "github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestServiceAsset(t *testing.T) { + client := newClient(t) + + t.Log("Creating Service") + loc := testServiceCreate(t, client, "test-site-2") + t.Logf("Created service at location: %s", loc) + + t.Log("Deleting Service Assets") + testServiceAssetDelete(t, client, loc) +} + +func testServiceAssetDelete(t *testing.T, client *gophercloud.ServiceClient, url string) { + deleteOpts := osServiceAssets.DeleteOpts{ + All: true, + } + err := serviceassets.Delete(client, url, deleteOpts).ExtractErr() + th.AssertNoErr(t, err) + t.Log("Successfully deleted all Service Assets") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go index 010bf4279cb..d7e6aa712c1 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go @@ -5,11 +5,11 @@ package v2 import ( "testing" + "github.com/rackspace/gophercloud/acceptance/tools" osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" "github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume" "github.com/rackspace/gophercloud/rackspace/compute/v2/servers" th "github.com/rackspace/gophercloud/testhelper" - "github.com/smashwilson/gophercloud/acceptance/tools" ) func TestBootFromVolume(t *testing.T) { @@ -41,6 +41,9 @@ func TestBootFromVolume(t *testing.T) { }).Extract() th.AssertNoErr(t, err) t.Logf("Created server: %+v\n", server) - //defer deleteServer(t, client, server) - t.Logf("Deleting server [%s]...", name) + defer deleteServer(t, client, server) + + getServer(t, client, server) + + listServers(t, client) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go index 511f0a96acd..a8b5937b6ee 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go @@ -39,11 +39,14 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, keyName strin name := tools.RandomString("Gophercloud-", 8) + pwd := tools.MakeNewPassword("") + opts := &servers.CreateOpts{ Name: name, ImageRef: options.imageID, FlavorRef: options.flavorID, DiskConfig: diskconfig.Manual, + AdminPass: pwd, } if keyName != "" { @@ -59,6 +62,8 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, keyName strin th.AssertNoErr(t, err) t.Logf("Server created successfully.") + th.CheckEquals(t, pwd, s.AdminPass) + return s } @@ -95,6 +100,18 @@ func getServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Serve logServer(t, details, -1) } +func updateServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Get") + + opts := os.UpdateOpts{ + Name: "updated-server", + } + updatedServer, err := servers.Update(client, server.ID, opts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "updated-server", updatedServer.Name) + logServer(t, updatedServer, -1) +} + func listServers(t *testing.T, client *gophercloud.ServiceClient) { t.Logf("> servers.List") @@ -120,7 +137,7 @@ func changeAdminPassword(t *testing.T, client *gophercloud.ServiceClient, server original := server.AdminPass t.Logf("Changing server password.") - err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).Extract() + err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).ExtractErr() th.AssertNoErr(t, err) err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300) @@ -131,7 +148,7 @@ func changeAdminPassword(t *testing.T, client *gophercloud.ServiceClient, server func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { t.Logf("> servers.Reboot") - err := servers.Reboot(client, server.ID, os.HardReboot).Extract() + err := servers.Reboot(client, server.ID, os.HardReboot).ExtractErr() th.AssertNoErr(t, err) err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300) @@ -192,6 +209,7 @@ func TestServerOperations(t *testing.T) { defer deleteServer(t, client, server) getServer(t, client, server) + updateServer(t, client, server) listServers(t, client) changeAdminPassword(t, client, server) rebootServer(t, client, server) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/pkg.go new file mode 100644 index 00000000000..5ec3cc8e833 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/pkg.go @@ -0,0 +1 @@ +package v2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/role_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/role_test.go new file mode 100644 index 00000000000..efaeb75cde8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/role_test.go @@ -0,0 +1,59 @@ +// +build acceptance identity roles + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/identity/v2/roles" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestRoles(t *testing.T) { + client := authenticatedClient(t) + + userID := createUser(t, client) + roleID := listRoles(t, client) + + addUserRole(t, client, userID, roleID) + + deleteUserRole(t, client, userID, roleID) + + deleteUser(t, client, userID) +} + +func listRoles(t *testing.T, client *gophercloud.ServiceClient) string { + var roleID string + + err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) { + roleList, err := os.ExtractRoles(page) + th.AssertNoErr(t, err) + + for _, role := range roleList { + t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name) + roleID = role.ID + } + + return true, nil + }) + + th.AssertNoErr(t, err) + + return roleID +} + +func addUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) { + err := roles.AddUserRole(client, userID, roleID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Added role %s to user %s", roleID, userID) +} + +func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) { + err := roles.DeleteUserRole(client, userID, roleID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Removed role %s from user %s", roleID, userID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/user_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/user_test.go new file mode 100644 index 00000000000..28c0c832bef --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/user_test.go @@ -0,0 +1,93 @@ +// +build acceptance identity + +package v2 + +import ( + "strconv" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + os "github.com/rackspace/gophercloud/openstack/identity/v2/users" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/identity/v2/users" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestUsers(t *testing.T) { + client := authenticatedClient(t) + + userID := createUser(t, client) + + listUsers(t, client) + + getUser(t, client, userID) + + updateUser(t, client, userID) + + resetApiKey(t, client, userID) + + deleteUser(t, client, userID) +} + +func createUser(t *testing.T, client *gophercloud.ServiceClient) string { + t.Log("Creating user") + + opts := users.CreateOpts{ + Username: tools.RandomString("user_", 5), + Enabled: os.Disabled, + Email: "new_user@foo.com", + } + + user, err := users.Create(client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created user %s", user.ID) + + return user.ID +} + +func listUsers(t *testing.T, client *gophercloud.ServiceClient) { + err := users.List(client).EachPage(func(page pagination.Page) (bool, error) { + userList, err := os.ExtractUsers(page) + th.AssertNoErr(t, err) + + for _, user := range userList { + t.Logf("Listing user: ID [%s] Username [%s] Email [%s] Enabled? [%s]", + user.ID, user.Username, user.Email, strconv.FormatBool(user.Enabled)) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + _, err := users.Get(client, userID).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting user %s", userID) +} + +func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + opts := users.UpdateOpts{Username: tools.RandomString("new_name", 5), Email: "new@foo.com"} + user, err := users.Update(client, userID, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Updated user %s: Username [%s] Email [%s]", userID, user.Username, user.Email) +} + +func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) { + res := users.Delete(client, userID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted user %s", userID) +} + +func resetApiKey(t *testing.T, client *gophercloud.ServiceClient, userID string) { + key, err := users.ResetAPIKey(client, userID).Extract() + th.AssertNoErr(t, err) + + if key.APIKey == "" { + t.Fatal("failed to reset API key for user") + } + + t.Logf("Reset API key for user %s to %s", key.Username, key.APIKey) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/acl_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/acl_test.go new file mode 100644 index 00000000000..7a380273c62 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/acl_test.go @@ -0,0 +1,94 @@ +// +build acceptance lbs + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/acl" + "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestACL(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 1) + lbID := ids[0] + + createACL(t, client, lbID) + + waitForLB(client, lbID, lbs.ACTIVE) + + networkIDs := showACL(t, client, lbID) + + deleteNetworkItem(t, client, lbID, networkIDs[0]) + waitForLB(client, lbID, lbs.ACTIVE) + + bulkDeleteACL(t, client, lbID, networkIDs[1:2]) + waitForLB(client, lbID, lbs.ACTIVE) + + deleteACL(t, client, lbID) + waitForLB(client, lbID, lbs.ACTIVE) + + deleteLB(t, client, lbID) +} + +func createACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + opts := acl.CreateOpts{ + acl.CreateOpt{Address: "206.160.163.21", Type: acl.DENY}, + acl.CreateOpt{Address: "206.160.165.11", Type: acl.DENY}, + acl.CreateOpt{Address: "206.160.165.12", Type: acl.DENY}, + acl.CreateOpt{Address: "206.160.165.13", Type: acl.ALLOW}, + } + + err := acl.Create(client, lbID, opts).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Created ACL items for LB %d", lbID) +} + +func showACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) []int { + ids := []int{} + + err := acl.List(client, lbID).EachPage(func(page pagination.Page) (bool, error) { + accessList, err := acl.ExtractAccessList(page) + th.AssertNoErr(t, err) + + for _, i := range accessList { + t.Logf("Listing network item: ID [%s] Address [%s] Type [%s]", i.ID, i.Address, i.Type) + ids = append(ids, i.ID) + } + + return true, nil + }) + th.AssertNoErr(t, err) + + return ids +} + +func deleteNetworkItem(t *testing.T, client *gophercloud.ServiceClient, lbID, itemID int) { + err := acl.Delete(client, lbID, itemID).ExtractErr() + + th.AssertNoErr(t, err) + + t.Logf("Deleted network item %d", itemID) +} + +func bulkDeleteACL(t *testing.T, client *gophercloud.ServiceClient, lbID int, items []int) { + err := acl.BulkDelete(client, lbID, items).ExtractErr() + + th.AssertNoErr(t, err) + + t.Logf("Deleted network items %s", intsToStr(items)) +} + +func deleteACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := acl.DeleteAll(client, lbID).ExtractErr() + + th.AssertNoErr(t, err) + + t.Logf("Deleted ACL from LB %d", lbID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/common.go new file mode 100644 index 00000000000..4ce05e69ca8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/common.go @@ -0,0 +1,62 @@ +// +build acceptance lbs + +package v1 + +import ( + "os" + "strconv" + "strings" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newProvider() (*gophercloud.ProviderClient, error) { + opts, err := rackspace.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + opts = tools.OnlyRS(opts) + + return rackspace.AuthenticatedClient(opts) +} + +func newClient() (*gophercloud.ServiceClient, error) { + provider, err := newProvider() + if err != nil { + return nil, err + } + + return rackspace.NewLBV1(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("RS_REGION"), + }) +} + +func newComputeClient() (*gophercloud.ServiceClient, error) { + provider, err := newProvider() + if err != nil { + return nil, err + } + + return rackspace.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("RS_REGION"), + }) +} + +func setup(t *testing.T) *gophercloud.ServiceClient { + client, err := newClient() + th.AssertNoErr(t, err) + + return client +} + +func intsToStr(ids []int) string { + strIDs := []string{} + for _, id := range ids { + strIDs = append(strIDs, strconv.Itoa(id)) + } + return strings.Join(strIDs, ", ") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/lb_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/lb_test.go new file mode 100644 index 00000000000..c67ddecad93 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/lb_test.go @@ -0,0 +1,214 @@ +// +build acceptance lbs + +package v1 + +import ( + "strconv" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs" + "github.com/rackspace/gophercloud/rackspace/lb/v1/vips" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestLBs(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 3) + id := ids[0] + + listLBProtocols(t, client) + + listLBAlgorithms(t, client) + + listLBs(t, client) + + getLB(t, client, id) + + checkLBLogging(t, client, id) + + checkErrorPage(t, client, id) + + getStats(t, client, id) + + updateLB(t, client, id) + + deleteLB(t, client, id) + + batchDeleteLBs(t, client, ids[1:]) +} + +func createLB(t *testing.T, client *gophercloud.ServiceClient, count int) []int { + ids := []int{} + + for i := 0; i < count; i++ { + opts := lbs.CreateOpts{ + Name: tools.RandomString("test_", 5), + Port: 80, + Protocol: "HTTP", + VIPs: []vips.VIP{ + vips.VIP{Type: vips.PUBLIC}, + }, + } + + lb, err := lbs.Create(client, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created LB %d - waiting for it to build...", lb.ID) + waitForLB(client, lb.ID, lbs.ACTIVE) + t.Logf("LB %d has reached ACTIVE state", lb.ID) + + ids = append(ids, lb.ID) + } + + return ids +} + +func waitForLB(client *gophercloud.ServiceClient, id int, state lbs.Status) { + gophercloud.WaitFor(60, func() (bool, error) { + lb, err := lbs.Get(client, id).Extract() + if err != nil { + return false, err + } + if lb.Status != state { + return false, nil + } + return true, nil + }) +} + +func listLBProtocols(t *testing.T, client *gophercloud.ServiceClient) { + err := lbs.ListProtocols(client).EachPage(func(page pagination.Page) (bool, error) { + pList, err := lbs.ExtractProtocols(page) + th.AssertNoErr(t, err) + + for _, p := range pList { + t.Logf("Listing protocol: Name [%s]", p.Name) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func listLBAlgorithms(t *testing.T, client *gophercloud.ServiceClient) { + err := lbs.ListAlgorithms(client).EachPage(func(page pagination.Page) (bool, error) { + aList, err := lbs.ExtractAlgorithms(page) + th.AssertNoErr(t, err) + + for _, a := range aList { + t.Logf("Listing algorithm: Name [%s]", a.Name) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func listLBs(t *testing.T, client *gophercloud.ServiceClient) { + err := lbs.List(client, lbs.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + lbList, err := lbs.ExtractLBs(page) + th.AssertNoErr(t, err) + + for _, lb := range lbList { + t.Logf("Listing LB: ID [%d] Name [%s] Protocol [%s] Status [%s] Node count [%d] Port [%d]", + lb.ID, lb.Name, lb.Protocol, lb.Status, lb.NodeCount, lb.Port) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getLB(t *testing.T, client *gophercloud.ServiceClient, id int) { + lb, err := lbs.Get(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting LB %d: Created [%s] VIPs [%#v] Logging [%#v] Persistence [%#v] SourceAddrs [%#v]", + lb.ID, lb.Created, lb.VIPs, lb.ConnectionLogging, lb.SessionPersistence, lb.SourceAddrs) +} + +func updateLB(t *testing.T, client *gophercloud.ServiceClient, id int) { + opts := lbs.UpdateOpts{ + Name: tools.RandomString("new_", 5), + Protocol: "TCP", + HalfClosed: gophercloud.Enabled, + Algorithm: "RANDOM", + Port: 8080, + Timeout: 100, + HTTPSRedirect: gophercloud.Disabled, + } + + err := lbs.Update(client, id, opts).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Updating LB %d - waiting for it to finish", id) + waitForLB(client, id, lbs.ACTIVE) + t.Logf("LB %d has reached ACTIVE state", id) +} + +func deleteLB(t *testing.T, client *gophercloud.ServiceClient, id int) { + err := lbs.Delete(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted LB %d", id) +} + +func batchDeleteLBs(t *testing.T, client *gophercloud.ServiceClient, ids []int) { + err := lbs.BulkDelete(client, ids).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted LB %s", intsToStr(ids)) +} + +func checkLBLogging(t *testing.T, client *gophercloud.ServiceClient, id int) { + err := lbs.EnableLogging(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Enabled logging for LB %d", id) + + res, err := lbs.IsLoggingEnabled(client, id) + th.AssertNoErr(t, err) + t.Logf("LB %d log enabled? %s", id, strconv.FormatBool(res)) + + waitForLB(client, id, lbs.ACTIVE) + + err = lbs.DisableLogging(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Disabled logging for LB %d", id) +} + +func checkErrorPage(t *testing.T, client *gophercloud.ServiceClient, id int) { + content, err := lbs.SetErrorPage(client, id, "New content!").Extract() + t.Logf("Set error page for LB %d", id) + + content, err = lbs.GetErrorPage(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Error page for LB %d: %s", id, content) + + err = lbs.DeleteErrorPage(client, id).ExtractErr() + t.Logf("Deleted error page for LB %d", id) +} + +func getStats(t *testing.T, client *gophercloud.ServiceClient, id int) { + waitForLB(client, id, lbs.ACTIVE) + + stats, err := lbs.GetStats(client, id).Extract() + th.AssertNoErr(t, err) + + t.Logf("Stats for LB %d: %#v", id, stats) +} + +func checkCaching(t *testing.T, client *gophercloud.ServiceClient, id int) { + err := lbs.EnableCaching(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Enabled caching for LB %d", id) + + res, err := lbs.IsContentCached(client, id) + th.AssertNoErr(t, err) + t.Logf("Is caching enabled for LB? %s", strconv.FormatBool(res)) + + err = lbs.DisableCaching(client, id).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Disabled caching for LB %d", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/monitor_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/monitor_test.go new file mode 100644 index 00000000000..c1a8e24dd9b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/monitor_test.go @@ -0,0 +1,60 @@ +// +build acceptance lbs + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs" + "github.com/rackspace/gophercloud/rackspace/lb/v1/monitors" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestMonitors(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 1) + lbID := ids[0] + + getMonitor(t, client, lbID) + + updateMonitor(t, client, lbID) + + deleteMonitor(t, client, lbID) + + deleteLB(t, client, lbID) +} + +func getMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + hm, err := monitors.Get(client, lbID).Extract() + th.AssertNoErr(t, err) + t.Logf("Health monitor for LB %d: Type [%s] Delay [%d] Timeout [%d] AttemptLimit [%d]", + lbID, hm.Type, hm.Delay, hm.Timeout, hm.AttemptLimit) +} + +func updateMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + opts := monitors.UpdateHTTPMonitorOpts{ + AttemptLimit: 3, + Delay: 10, + Timeout: 10, + BodyRegex: "hello is it me you're looking for", + Path: "/foo", + StatusRegex: "200", + Type: monitors.HTTP, + } + + err := monitors.Update(client, lbID, opts).ExtractErr() + th.AssertNoErr(t, err) + + waitForLB(client, lbID, lbs.ACTIVE) + t.Logf("Updated monitor for LB %d", lbID) +} + +func deleteMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := monitors.Delete(client, lbID).ExtractErr() + th.AssertNoErr(t, err) + + waitForLB(client, lbID, lbs.ACTIVE) + t.Logf("Deleted monitor for LB %d", lbID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/node_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/node_test.go new file mode 100644 index 00000000000..18b9fe71ce3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/node_test.go @@ -0,0 +1,175 @@ +// +build acceptance lbs + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/servers" + "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs" + "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNodes(t *testing.T) { + client := setup(t) + + serverIP := findServer(t) + ids := createLB(t, client, 1) + lbID := ids[0] + + nodeID := addNodes(t, client, lbID, serverIP) + + listNodes(t, client, lbID) + + getNode(t, client, lbID, nodeID) + + updateNode(t, client, lbID, nodeID) + + listEvents(t, client, lbID) + + deleteNode(t, client, lbID, nodeID) + + waitForLB(client, lbID, lbs.ACTIVE) + deleteLB(t, client, lbID) +} + +func findServer(t *testing.T) string { + var serverIP string + + client, err := newComputeClient() + th.AssertNoErr(t, err) + + err = servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + sList, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + + for _, s := range sList { + serverIP = s.AccessIPv4 + t.Logf("Found an existing server: ID [%s] Public IP [%s]", s.ID, serverIP) + break + } + + return true, nil + }) + th.AssertNoErr(t, err) + + if serverIP == "" { + t.Log("No server found, creating one") + + imageRef := os.Getenv("RS_IMAGE_ID") + if imageRef == "" { + t.Fatalf("OS var RS_IMAGE_ID undefined") + } + flavorRef := os.Getenv("RS_FLAVOR_ID") + if flavorRef == "" { + t.Fatalf("OS var RS_FLAVOR_ID undefined") + } + + opts := &servers.CreateOpts{ + Name: tools.RandomString("lb_test_", 5), + ImageRef: imageRef, + FlavorRef: flavorRef, + DiskConfig: diskconfig.Manual, + } + + s, err := servers.Create(client, opts).Extract() + th.AssertNoErr(t, err) + serverIP = s.AccessIPv4 + + t.Logf("Created server %s, waiting for it to build", s.ID) + err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + t.Logf("Server created successfully.") + } + + return serverIP +} + +func addNodes(t *testing.T, client *gophercloud.ServiceClient, lbID int, serverIP string) int { + opts := nodes.CreateOpts{ + nodes.CreateOpt{ + Address: serverIP, + Port: 80, + Condition: nodes.ENABLED, + Type: nodes.PRIMARY, + }, + } + + page := nodes.Create(client, lbID, opts) + + nodeList, err := page.ExtractNodes() + th.AssertNoErr(t, err) + + var nodeID int + for _, n := range nodeList { + nodeID = n.ID + } + if nodeID == 0 { + t.Fatalf("nodeID could not be extracted from create response") + } + + t.Logf("Added node %d to LB %d", nodeID, lbID) + waitForLB(client, lbID, lbs.ACTIVE) + + return nodeID +} + +func listNodes(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := nodes.List(client, lbID, nil).EachPage(func(page pagination.Page) (bool, error) { + nodeList, err := nodes.ExtractNodes(page) + th.AssertNoErr(t, err) + + for _, n := range nodeList { + t.Logf("Listing node: ID [%d] Address [%s:%d] Status [%s]", n.ID, n.Address, n.Port, n.Status) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func getNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) { + node, err := nodes.Get(client, lbID, nodeID).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting node %d: Type [%s] Weight [%d]", nodeID, node.Type, node.Weight) +} + +func updateNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) { + opts := nodes.UpdateOpts{ + Weight: gophercloud.IntToPointer(10), + Condition: nodes.DRAINING, + Type: nodes.SECONDARY, + } + err := nodes.Update(client, lbID, nodeID, opts).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Updated node %d", nodeID) + waitForLB(client, lbID, lbs.ACTIVE) +} + +func listEvents(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + pager := nodes.ListEvents(client, lbID, nodes.ListEventsOpts{}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + eventList, err := nodes.ExtractNodeEvents(page) + th.AssertNoErr(t, err) + + for _, e := range eventList { + t.Logf("Listing events for node %d: Type [%s] Msg [%s] Severity [%s] Date [%s]", + e.NodeID, e.Type, e.DetailedMessage, e.Severity, e.Created) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func deleteNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) { + err := nodes.Delete(client, lbID, nodeID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted node %d", nodeID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/session_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/session_test.go new file mode 100644 index 00000000000..8d85655f6b1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/session_test.go @@ -0,0 +1,47 @@ +// +build acceptance lbs + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSession(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 1) + lbID := ids[0] + + getSession(t, client, lbID) + + enableSession(t, client, lbID) + waitForLB(client, lbID, "ACTIVE") + + disableSession(t, client, lbID) + waitForLB(client, lbID, "ACTIVE") + + deleteLB(t, client, lbID) +} + +func getSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + sp, err := sessions.Get(client, lbID).Extract() + th.AssertNoErr(t, err) + t.Logf("Session config: Type [%s]", sp.Type) +} + +func enableSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + opts := sessions.CreateOpts{Type: sessions.HTTPCOOKIE} + err := sessions.Enable(client, lbID, opts).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Enable %s sessions for %d", opts.Type, lbID) +} + +func disableSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := sessions.Disable(client, lbID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Disable sessions for %d", lbID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/throttle_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/throttle_test.go new file mode 100644 index 00000000000..1cc12356caf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/throttle_test.go @@ -0,0 +1,53 @@ +// +build acceptance lbs + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestThrottle(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 1) + lbID := ids[0] + + getThrottleConfig(t, client, lbID) + + createThrottleConfig(t, client, lbID) + waitForLB(client, lbID, "ACTIVE") + + deleteThrottleConfig(t, client, lbID) + waitForLB(client, lbID, "ACTIVE") + + deleteLB(t, client, lbID) +} + +func getThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + sp, err := throttle.Get(client, lbID).Extract() + th.AssertNoErr(t, err) + t.Logf("Throttle config: MaxConns [%s]", sp.MaxConnections) +} + +func createThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + opts := throttle.CreateOpts{ + MaxConnections: 200, + MaxConnectionRate: 100, + MinConnections: 0, + RateInterval: 10, + } + + err := throttle.Create(client, lbID, opts).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Enable throttling for %d", lbID) +} + +func deleteThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := throttle.Delete(client, lbID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Disable throttling for %d", lbID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/vip_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/vip_test.go new file mode 100644 index 00000000000..bc0c2a89f2a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/lb/v1/vip_test.go @@ -0,0 +1,83 @@ +// +build acceptance lbs + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs" + "github.com/rackspace/gophercloud/rackspace/lb/v1/vips" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVIPs(t *testing.T) { + client := setup(t) + + ids := createLB(t, client, 1) + lbID := ids[0] + + listVIPs(t, client, lbID) + + vipIDs := addVIPs(t, client, lbID, 3) + + deleteVIP(t, client, lbID, vipIDs[0]) + + bulkDeleteVIPs(t, client, lbID, vipIDs[1:]) + + waitForLB(client, lbID, lbs.ACTIVE) + deleteLB(t, client, lbID) +} + +func listVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID int) { + err := vips.List(client, lbID).EachPage(func(page pagination.Page) (bool, error) { + vipList, err := vips.ExtractVIPs(page) + th.AssertNoErr(t, err) + + for _, vip := range vipList { + t.Logf("Listing VIP: ID [%s] Address [%s] Type [%s] Version [%s]", + vip.ID, vip.Address, vip.Type, vip.Version) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func addVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID, count int) []int { + ids := []int{} + + for i := 0; i < count; i++ { + opts := vips.CreateOpts{ + Type: vips.PUBLIC, + Version: vips.IPV6, + } + + vip, err := vips.Create(client, lbID, opts).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created VIP %d", vip.ID) + + waitForLB(client, lbID, lbs.ACTIVE) + + ids = append(ids, vip.ID) + } + + return ids +} + +func deleteVIP(t *testing.T, client *gophercloud.ServiceClient, lbID, vipID int) { + err := vips.Delete(client, lbID, vipID).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Deleted VIP %d", vipID) + + waitForLB(client, lbID, lbs.ACTIVE) +} + +func bulkDeleteVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID int, ids []int) { + err := vips.BulkDelete(client, lbID, ids).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted VIPs %s", intsToStr(ids)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/common.go new file mode 100644 index 00000000000..81704187fef --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/common.go @@ -0,0 +1,39 @@ +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +var Client *gophercloud.ServiceClient + +func NewClient() (*gophercloud.ServiceClient, error) { + opts, err := rackspace.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + provider, err := rackspace.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return rackspace.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Name: "cloudNetworks", + Region: os.Getenv("RS_REGION"), + }) +} + +func Setup(t *testing.T) { + client, err := NewClient() + th.AssertNoErr(t, err) + Client = client +} + +func Teardown() { + Client = nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/network_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/network_test.go new file mode 100644 index 00000000000..3862123bfd9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/network_test.go @@ -0,0 +1,65 @@ +// +build acceptance networking + +package v2 + +import ( + "strconv" + "testing" + + os "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/networking/v2/networks" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNetworkCRUDOperations(t *testing.T) { + Setup(t) + defer Teardown() + + // Create a network + n, err := networks.Create(Client, os.CreateOpts{Name: "sample_network", AdminStateUp: os.Up}).Extract() + th.AssertNoErr(t, err) + defer networks.Delete(Client, n.ID) + th.AssertEquals(t, "sample_network", n.Name) + th.AssertEquals(t, true, n.AdminStateUp) + networkID := n.ID + + // List networks + pager := networks.List(Client, os.ListOpts{Limit: 2}) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + networkList, err := os.ExtractNetworks(page) + th.AssertNoErr(t, err) + + for _, n := range networkList { + t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]", + n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared)) + } + + return true, nil + }) + th.CheckNoErr(t, err) + + // Get a network + if networkID == "" { + t.Fatalf("In order to retrieve a network, the NetworkID must be set") + } + n, err = networks.Get(Client, networkID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "ACTIVE", n.Status) + th.AssertDeepEquals(t, []string{}, n.Subnets) + th.AssertEquals(t, "sample_network", n.Name) + th.AssertEquals(t, true, n.AdminStateUp) + th.AssertEquals(t, false, n.Shared) + th.AssertEquals(t, networkID, n.ID) + + // Update network + n, err = networks.Update(Client, networkID, os.UpdateOpts{Name: "new_network_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "new_network_name", n.Name) + + // Delete network + res := networks.Delete(Client, networkID) + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/port_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/port_test.go new file mode 100644 index 00000000000..3c42bb20cd3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/port_test.go @@ -0,0 +1,116 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + osPorts "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + osSubnets "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/networking/v2/networks" + "github.com/rackspace/gophercloud/rackspace/networking/v2/ports" + "github.com/rackspace/gophercloud/rackspace/networking/v2/subnets" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestPortCRUD(t *testing.T) { + Setup(t) + defer Teardown() + + // Setup network + t.Log("Setting up network") + networkID, err := createNetwork() + th.AssertNoErr(t, err) + defer networks.Delete(Client, networkID) + + // Setup subnet + t.Logf("Setting up subnet on network %s", networkID) + subnetID, err := createSubnet(networkID) + th.AssertNoErr(t, err) + defer subnets.Delete(Client, subnetID) + + // Create port + t.Logf("Create port based on subnet %s", subnetID) + portID := createPort(t, networkID, subnetID) + + // List ports + t.Logf("Listing all ports") + listPorts(t) + + // Get port + if portID == "" { + t.Fatalf("In order to retrieve a port, the portID must be set") + } + p, err := ports.Get(Client, portID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, portID, p.ID) + + // Update port + p, err = ports.Update(Client, portID, osPorts.UpdateOpts{Name: "new_port_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "new_port_name", p.Name) + + // Delete port + res := ports.Delete(Client, portID) + th.AssertNoErr(t, res.Err) +} + +func createPort(t *testing.T, networkID, subnetID string) string { + enable := true + opts := osPorts.CreateOpts{ + NetworkID: networkID, + Name: "my_port", + AdminStateUp: &enable, + FixedIPs: []osPorts.IP{osPorts.IP{SubnetID: subnetID}}, + } + p, err := ports.Create(Client, opts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, networkID, p.NetworkID) + th.AssertEquals(t, "my_port", p.Name) + th.AssertEquals(t, true, p.AdminStateUp) + + return p.ID +} + +func listPorts(t *testing.T) { + count := 0 + pager := ports.List(Client, osPorts.ListOpts{}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("--- Page ---") + + portList, err := osPorts.ExtractPorts(page) + th.AssertNoErr(t, err) + + for _, p := range portList { + t.Logf("Port: ID [%s] Name [%s] Status [%s] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]", + p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups) + } + + return true, nil + }) + + th.CheckNoErr(t, err) + + if count == 0 { + t.Logf("No pages were iterated over when listing ports") + } +} + +func createNetwork() (string, error) { + res, err := networks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network", AdminStateUp: osNetworks.Up}).Extract() + return res.ID, err +} + +func createSubnet(networkID string) (string, error) { + s, err := subnets.Create(Client, osSubnets.CreateOpts{ + NetworkID: networkID, + CIDR: "192.168.199.0/24", + IPVersion: osSubnets.IPv4, + Name: "my_subnet", + EnableDHCP: osSubnets.Down, + }).Extract() + return s.ID, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/subnet_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/subnet_test.go new file mode 100644 index 00000000000..c4014320a4c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/subnet_test.go @@ -0,0 +1,84 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + osSubnets "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/networking/v2/networks" + "github.com/rackspace/gophercloud/rackspace/networking/v2/subnets" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListSubnets(t *testing.T) { + Setup(t) + defer Teardown() + + pager := subnets.List(Client, osSubnets.ListOpts{Limit: 2}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + subnetList, err := osSubnets.ExtractSubnets(page) + th.AssertNoErr(t, err) + + for _, s := range subnetList { + t.Logf("Subnet: ID [%s] Name [%s] IP Version [%d] CIDR [%s] GatewayIP [%s]", + s.ID, s.Name, s.IPVersion, s.CIDR, s.GatewayIP) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} + +func TestSubnetCRUD(t *testing.T) { + Setup(t) + defer Teardown() + + // Setup network + t.Log("Setting up network") + n, err := networks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network", AdminStateUp: osNetworks.Up}).Extract() + th.AssertNoErr(t, err) + networkID := n.ID + defer networks.Delete(Client, networkID) + + // Create subnet + t.Log("Create subnet") + enable := false + opts := osSubnets.CreateOpts{ + NetworkID: networkID, + CIDR: "192.168.199.0/24", + IPVersion: osSubnets.IPv4, + Name: "my_subnet", + EnableDHCP: &enable, + } + s, err := subnets.Create(Client, opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, networkID, s.NetworkID) + th.AssertEquals(t, "192.168.199.0/24", s.CIDR) + th.AssertEquals(t, 4, s.IPVersion) + th.AssertEquals(t, "my_subnet", s.Name) + th.AssertEquals(t, false, s.EnableDHCP) + subnetID := s.ID + + // Get subnet + t.Log("Getting subnet") + s, err = subnets.Get(Client, subnetID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, subnetID, s.ID) + + // Update subnet + t.Log("Update subnet") + s, err = subnets.Update(Client, subnetID, osSubnets.UpdateOpts{Name: "new_subnet_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "new_subnet_name", s.Name) + + // Delete subnet + t.Log("Delete subnet") + res := subnets.Delete(Client, subnetID) + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go index 145e4e0482c..8b3cde45e45 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go @@ -13,11 +13,11 @@ func TestAccounts(t *testing.T) { c, err := createClient(t, false) th.AssertNoErr(t, err) - updateres := raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) - th.AssertNoErr(t, updateres.Err) - t.Logf("Headers from Update Account request: %+v\n", updateres.Header) + updateHeaders, err := raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}).Extract() + th.AssertNoErr(t, err) + t.Logf("Update Account Response Headers: %+v\n", updateHeaders) defer func() { - updateres = raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": ""}}) + updateres := raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": ""}}) th.AssertNoErr(t, updateres.Err) metadata, err := raxAccounts.Get(c).ExtractMetadata() th.AssertNoErr(t, err) @@ -25,8 +25,13 @@ func TestAccounts(t *testing.T) { th.CheckEquals(t, metadata["White"], "") }() - metadata, err := raxAccounts.Get(c).ExtractMetadata() - th.AssertNoErr(t, err) + getResp := raxAccounts.Get(c) + th.AssertNoErr(t, getResp.Err) + + getHeaders, _ := getResp.Extract() + t.Logf("Get Account Response Headers: %+v\n", getHeaders) + + metadata, _ := getResp.ExtractMetadata() t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) th.CheckEquals(t, metadata["White"], "mountains") diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go index e1bf38b16fe..0f56f4978af 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go @@ -26,10 +26,11 @@ func TestCDNContainers(t *testing.T) { raxCDNClient, err := createClient(t, true) th.AssertNoErr(t, err) - - r := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900}) - th.AssertNoErr(t, r.Err) - t.Logf("Headers from Enable CDN Container request: %+v\n", r.Header) + enableRes := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900}) + t.Logf("Header map from Enable CDN Container request: %+v\n", enableRes.Header) + enableHeader, err := enableRes.Extract() + th.AssertNoErr(t, err) + t.Logf("Headers from Enable CDN Container request: %+v\n", enableHeader) t.Logf("Container Names available to the currently issued token:") count := 0 @@ -51,11 +52,15 @@ func TestCDNContainers(t *testing.T) { t.Errorf("No CDN containers listed for your current token.") } - updateres := raxCDNContainers.Update(raxCDNClient, "gophercloud-test", raxCDNContainers.UpdateOpts{CDNEnabled: false}) - th.AssertNoErr(t, updateres.Err) - t.Logf("Headers from Update CDN Container request: %+v\n", updateres.Header) - - metadata, err := raxCDNContainers.Get(raxCDNClient, "gophercloud-test").ExtractMetadata() + updateOpts := raxCDNContainers.UpdateOpts{XCDNEnabled: raxCDNContainers.Disabled, XLogRetention: raxCDNContainers.Enabled} + updateHeader, err := raxCDNContainers.Update(raxCDNClient, "gophercloud-test", updateOpts).Extract() th.AssertNoErr(t, err) - t.Logf("Headers from Get CDN Container request (after update): %+v\n", metadata) + t.Logf("Headers from Update CDN Container request: %+v\n", updateHeader) + + getRes := raxCDNContainers.Get(raxCDNClient, "gophercloud-test") + getHeader, err := getRes.Extract() + th.AssertNoErr(t, err) + t.Logf("Headers from Get CDN Container request (after update): %+v\n", getHeader) + metadata, err := getRes.ExtractMetadata() + t.Logf("Metadata from Get CDN Container request (after update): %+v\n", metadata) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go index a7339cf3884..c89551373f1 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go @@ -57,29 +57,34 @@ func TestContainers(t *testing.T) { t.Errorf("No containers listed for your current token.") } - createres := raxContainers.Create(c, "gophercloud-test", nil) - th.AssertNoErr(t, createres.Err) + createHeader, err := raxContainers.Create(c, "gophercloud-test", nil).Extract() + th.AssertNoErr(t, err) + t.Logf("Headers from Create Container request: %+v\n", createHeader) defer func() { - res := raxContainers.Delete(c, "gophercloud-test") - th.AssertNoErr(t, res.Err) + deleteres := raxContainers.Delete(c, "gophercloud-test") + deleteHeader, err := deleteres.Extract() + th.AssertNoErr(t, err) + t.Logf("Headers from Delete Container request: %+v\n", deleteres.Header) + t.Logf("Headers from Delete Container request: %+v\n", deleteHeader) }() - updateres := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) - th.AssertNoErr(t, updateres.Err) - t.Logf("Headers from Update Account request: %+v\n", updateres.Header) + updateHeader, err := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}).Extract() + th.AssertNoErr(t, err) + t.Logf("Headers from Update Container request: %+v\n", updateHeader) defer func() { res := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": ""}}) th.AssertNoErr(t, res.Err) metadata, err := raxContainers.Get(c, "gophercloud-test").ExtractMetadata() th.AssertNoErr(t, err) - t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) + t.Logf("Metadata from Get Container request (after update reverted): %+v\n", metadata) th.CheckEquals(t, metadata["White"], "") }() getres := raxContainers.Get(c, "gophercloud-test") - t.Logf("Headers from Get Account request (after update): %+v\n", getres.Header) - metadata, err := getres.ExtractMetadata() + getHeader, err := getres.Extract() th.AssertNoErr(t, err) - t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) + t.Logf("Headers from Get Container request (after update): %+v\n", getHeader) + metadata, err := getres.ExtractMetadata() + t.Logf("Metadata from Get Container request (after update): %+v\n", metadata) th.CheckEquals(t, metadata["White"], "mountains") } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go index 462f2847dbc..585dea7696a 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go @@ -21,6 +21,7 @@ func TestObjects(t *testing.T) { th.AssertNoErr(t, res.Err) defer func() { + t.Logf("Deleting container...") res := raxContainers.Delete(c, "gophercloud-test") th.AssertNoErr(t, res.Err) }() @@ -29,7 +30,9 @@ func TestObjects(t *testing.T) { options := &osObjects.CreateOpts{ContentType: "text/plain"} createres := raxObjects.Create(c, "gophercloud-test", "o1", content, options) th.AssertNoErr(t, createres.Err) + defer func() { + t.Logf("Deleting object o1...") res := raxObjects.Delete(c, "gophercloud-test", "o1", nil) th.AssertNoErr(t, res.Err) }() @@ -80,6 +83,7 @@ func TestObjects(t *testing.T) { copyres := raxObjects.Copy(c, "gophercloud-test", "o1", &raxObjects.CopyOpts{Destination: "gophercloud-test/o2"}) th.AssertNoErr(t, copyres.Err) defer func() { + t.Logf("Deleting object o2...") res := raxObjects.Delete(c, "gophercloud-test", "o2", nil) th.AssertNoErr(t, res.Err) }() @@ -99,7 +103,7 @@ func TestObjects(t *testing.T) { metadata, err := raxObjects.Get(c, "gophercloud-test", "o2", nil).ExtractMetadata() th.AssertNoErr(t, err) t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) - th.CheckEquals(t, metadata["White"], "") + th.CheckEquals(t, "", metadata["White"]) }() getres := raxObjects.Get(c, "gophercloud-test", "o2", nil) @@ -108,5 +112,13 @@ func TestObjects(t *testing.T) { metadata, err := getres.ExtractMetadata() th.AssertNoErr(t, err) t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) - th.CheckEquals(t, metadata["White"], "mountains") + th.CheckEquals(t, "mountains", metadata["White"]) + + createTempURLOpts := osObjects.CreateTempURLOpts{ + Method: osObjects.GET, + TTL: 600, + } + tempURL, err := raxObjects.CreateTempURL(c, "gophercloud-test", "o1", createTempURLOpts) + th.AssertNoErr(t, err) + t.Logf("TempURL for object (%s): %s", "o1", tempURL) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go index 61b1d7a6992..35679b728c3 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go @@ -1,10 +1,11 @@ -// +build acceptance +// +build acceptance common package tools import ( "crypto/rand" "errors" + mrand "math/rand" "os" "time" @@ -72,6 +73,12 @@ func RandomString(prefix string, n int) string { return prefix + string(bytes) } +// RandomInt will return a random integer between a specified range. +func RandomInt(min, max int) int { + mrand.Seed(time.Now().Unix()) + return mrand.Intn(max-min) + min +} + // Elide returns the first bit of its input string with a suffix of "..." if it's longer than // a comfortable 40 characters. func Elide(value string) string { diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go index bc0ef65a91b..19ce5d4a99d 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go @@ -1,20 +1,28 @@ package gophercloud -// AuthOptions allows anyone calling Authenticate to supply the required access -// credentials. Its fields are the union of those recognized by each identity -// implementation and provider. +/* +AuthOptions stores information needed to authenticate to an OpenStack cluster. +You can populate one manually, or use a provider's AuthOptionsFromEnv() function +to read relevant information from the standard environment variables. Pass one +to a provider's AuthenticatedClient function to authenticate and obtain a +ProviderClient representing an active session on that provider. + +Its fields are the union of those recognized by each identity implementation and +provider. +*/ type AuthOptions struct { // IdentityEndpoint specifies the HTTP endpoint that is required to work with - // the Identity API of the appropriate version. Required by the identity - // services, but often populated by a provider Client. + // the Identity API of the appropriate version. While it's ultimately needed by + // all of the identity services, it will often be populated by a provider-level + // function. IdentityEndpoint string // Username is required if using Identity V2 API. Consult with your provider's // control panel to discover your account's username. In Identity V3, either - // UserID or a combination of Username and DomainID or DomainName. + // UserID or a combination of Username and DomainID or DomainName are needed. Username, UserID string - // Exactly one of Password or ApiKey is required for the Identity V2 and V3 + // Exactly one of Password or APIKey is required for the Identity V2 and V3 // APIs. Consult with your provider's control panel to discover your account's // preferred method of authentication. Password, APIKey string @@ -25,7 +33,7 @@ type AuthOptions struct { // The TenantID and TenantName fields are optional for the Identity V2 API. // Some providers allow you to specify a TenantName instead of the TenantId. - // Some require both. Your provider's authentication policies will determine + // Some require both. Your provider's authentication policies will determine // how these fields influence authentication. TenantID, TenantName string @@ -34,5 +42,7 @@ type AuthOptions struct { // re-authenticate automatically if/when your token expires. If you set it to // false, it will not cache these settings, but re-authentication will not be // possible. This setting defaults to false. + // + // This setting is speculative and is currently not respected! AllowReauth bool } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go index 1a1faa5fe0e..856a23382e9 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go @@ -2,10 +2,9 @@ package gophercloud import "time" -// AuthResults encapsulates the raw results from an authentication request. As OpenStack allows -// extensions to influence the structure returned in ways that Gophercloud cannot predict at -// compile-time, you should use type-safe accessors to work with the data represented by this type, -// such as ServiceCatalog and TokenID. +// AuthResults [deprecated] is a leftover type from the v0.x days. It was +// intended to describe common functionality among identity service results, but +// is not actually used anywhere. type AuthResults interface { // TokenID returns the token's ID value from the authentication response. TokenID() (string, error) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/doc.go new file mode 100644 index 00000000000..fb81a9d8f17 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/doc.go @@ -0,0 +1,67 @@ +/* +Package gophercloud provides a multi-vendor interface to OpenStack-compatible +clouds. The library has a three-level hierarchy: providers, services, and +resources. + +Provider structs represent the service providers that offer and manage a +collection of services. Examples of providers include: OpenStack, Rackspace, +HP. These are defined like so: + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://my-openstack.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", + } + + provider, err := openstack.AuthenticatedClient(opts) + +Service structs are specific to a provider and handle all of the logic and +operations for a particular OpenStack service. Examples of services include: +Compute, Object Storage, Block Storage. In order to define one, you need to +pass in the parent provider, like so: + + opts := gophercloud.EndpointOpts{Region: "RegionOne"} + + client := openstack.NewComputeV2(provider, opts) + +Resource structs are the domain models that services make use of in order +to work with and represent the state of API resources: + + server, err := servers.Get(client, "{serverId}").Extract() + +Intermediate Result structs are returned for API operations, which allow +generic access to the HTTP headers, response body, and any errors associated +with the network transaction. To turn a result into a usable resource struct, +you must call the Extract method which is chained to the response, or an +Extract function from an applicable extension: + + result := servers.Get(client, "{serverId}") + + // Attempt to extract the disk configuration from the OS-DCF disk config + // extension: + config, err := diskconfig.ExtractGet(result) + +All requests that enumerate a collection return a Pager struct that is used to +iterate through the results one page at a time. Use the EachPage method on that +Pager to handle each successive Page in a closure, then use the appropriate +extraction method from that request's package to interpret that Page as a slice +of results: + + err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) { + s, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + + // Handle the []servers.Server slice. + + // Return "false" or an error to prematurely stop fetching new pages. + return true, nil + }) + +This top-level package contains utility functions and data types that are used +throughout the provider and service packages. Of particular note for end users +are the AuthOptions and EndpointOpts structs. +*/ +package gophercloud diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go index b6f6b4804fc..5189431212c 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go @@ -3,58 +3,85 @@ package gophercloud import "errors" var ( - // ErrServiceNotFound is returned when no service matches the EndpointOpts. + // ErrServiceNotFound is returned when no service in a service catalog matches + // the provided EndpointOpts. This is generally returned by provider service + // factory methods like "NewComputeV2()" and can mean that a service is not + // enabled for your account. ErrServiceNotFound = errors.New("No suitable service could be found in the service catalog.") - // ErrEndpointNotFound is returned when no available endpoints match the provided EndpointOpts. + // ErrEndpointNotFound is returned when no available endpoints match the + // provided EndpointOpts. This is also generally returned by provider service + // factory methods, and usually indicates that a region was specified + // incorrectly. ErrEndpointNotFound = errors.New("No suitable endpoint could be found in the service catalog.") ) -// Availability indicates whether a specific service endpoint is accessible. -// Identity v2 lists these as different kinds of URLs ("adminURL", -// "internalURL", and "publicURL"), while v3 lists them as "Interfaces". +// Availability indicates to whom a specific service endpoint is accessible: +// the internet at large, internal networks only, or only to administrators. +// Different identity services use different terminology for these. Identity v2 +// lists them as different kinds of URLs within the service catalog ("adminURL", +// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an +// endpoint's response. type Availability string const ( - // AvailabilityAdmin makes an endpoint only available to administrators. + // AvailabilityAdmin indicates that an endpoint is only available to + // administrators. AvailabilityAdmin Availability = "admin" - // AvailabilityPublic makes an endpoint available to everyone. + // AvailabilityPublic indicates that an endpoint is available to everyone on + // the internet. AvailabilityPublic Availability = "public" - // AvailabilityInternal makes an endpoint only available within the cluster. + // AvailabilityInternal indicates that an endpoint is only available within + // the cluster's internal network. AvailabilityInternal Availability = "internal" ) -// EndpointOpts contains options for finding an endpoint for an Openstack client. +// EndpointOpts specifies search criteria used by queries against an +// OpenStack service catalog. The options must contain enough information to +// unambiguously identify one, and only one, endpoint within the catalog. +// +// Usually, these are passed to service client factory functions in a provider +// package, like "rackspace.NewComputeV2()". type EndpointOpts struct { - // Type is the service type for the client (e.g., "compute", "object-store"). - // Required. + // Type [required] is the service type for the client (e.g., "compute", + // "object-store"). Generally, this will be supplied by the service client + // function, but a user-given value will be honored if provided. Type string - // Name is the service name for the client (e.g., "nova") as it appears in - // the service catalog. Services can have the same Type but a different Name, - // which is why both Type and Name are sometimes needed. Optional. + // Name [optional] is the service name for the client (e.g., "nova") as it + // appears in the service catalog. Services can have the same Type but a + // different Name, which is why both Type and Name are sometimes needed. Name string - // Region is the geographic region in which the service resides. Required only - // for services that span multiple regions. + // Region [required] is the geographic region in which the endpoint resides, + // generally specifying which datacenter should house your resources. + // Required only for services that span multiple regions. Region string - // Availability is the visibility of the endpoint to be returned. Valid types - // are: AvailabilityPublic, AvailabilityInternal, or AvailabilityAdmin. - // Availability is not required, and defaults to AvailabilityPublic. - // Not all providers or services offer all Availability options. + // Availability [optional] is the visibility of the endpoint to be returned. + // Valid types include the constants AvailabilityPublic, AvailabilityInternal, + // or AvailabilityAdmin from this package. + // + // Availability is not required, and defaults to AvailabilityPublic. Not all + // providers or services offer all Availability options. Availability Availability } -// EndpointLocator is a function that describes how to locate a single endpoint -// from a service catalog for a specific ProviderClient. It should be set -// during ProviderClient authentication and used to discover related ServiceClients. +/* +EndpointLocator is an internal function to be used by provider implementations. + +It provides an implementation that locates a single endpoint from a service +catalog for a specific ProviderClient based on user-provided EndpointOpts. The +provider then uses it to discover related ServiceClients. +*/ type EndpointLocator func(EndpointOpts) (string, error) -// ApplyDefaults sets EndpointOpts fields if not already set. Currently, -// EndpointOpts.Availability defaults to the public endpoint. +// ApplyDefaults is an internal method to be used by provider implementations. +// +// It sets EndpointOpts fields if not already set, including a default type. +// Currently, EndpointOpts.Availability defaults to the public endpoint. func (eo *EndpointOpts) ApplyDefaults(t string) { if eo.Type == "" { eo.Type = t diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util_test.go deleted file mode 100644 index a4c4c8282e0..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package snapshots - -import ( - "fmt" - "net/http" - "testing" - "time" - - th "github.com/rackspace/gophercloud/testhelper" - "github.com/rackspace/gophercloud/testhelper/client" -) - -func TestWaitForStatus(t *testing.T) { - th.SetupHTTP() - defer th.TeardownHTTP() - - th.Mux.HandleFunc("/snapshots/1234", func(w http.ResponseWriter, r *http.Request) { - time.Sleep(2 * time.Second) - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, ` - { - "snapshot": { - "display_name": "snapshot-001", - "id": "1234", - "status":"available" - } - }`) - }) - - err := WaitForStatus(client.ServiceClient(), "1234", "available", 0) - if err == nil { - t.Errorf("Expected error: 'Time Out in WaitFor'") - } - - err = WaitForStatus(client.ServiceClient(), "1234", "available", 3) - th.CheckNoErr(t, err) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go index c6ddbb5167f..aff60bca148 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go @@ -28,7 +28,7 @@ type Volume struct { CreatedAt string `mapstructure:"created_at"` // Human-readable description for the volume. - Description string `mapstructure:"display_discription"` + Description string `mapstructure:"display_description"` // The type of volume to create, either SATA or SSD. VolumeType string `mapstructure:"volume_type"` diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util_test.go deleted file mode 100644 index 24ef3b6190c..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package volumes - -import ( - "fmt" - "net/http" - "testing" - "time" - - th "github.com/rackspace/gophercloud/testhelper" - "github.com/rackspace/gophercloud/testhelper/client" -) - -func TestWaitForStatus(t *testing.T) { - th.SetupHTTP() - defer th.TeardownHTTP() - - th.Mux.HandleFunc("/volumes/1234", func(w http.ResponseWriter, r *http.Request) { - time.Sleep(3 * time.Second) - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, ` - { - "volume": { - "display_name": "vol-001", - "id": "1234", - "status":"available" - } - }`) - }) - - err := WaitForStatus(client.ServiceClient(), "1234", "available", 0) - if err == nil { - t.Errorf("Expected error: 'Time Out in WaitFor'") - } - - err = WaitForStatus(client.ServiceClient(), "1234", "available", 6) - th.CheckNoErr(t, err) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/doc.go new file mode 100644 index 00000000000..f78d4f73551 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/doc.go @@ -0,0 +1,4 @@ +// Package base provides information and interaction with the base API +// resource in the OpenStack CDN service. This API resource allows for +// retrieving the Home Document and pinging the root URL. +package base diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/fixtures.go new file mode 100644 index 00000000000..19b5ece4614 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/fixtures.go @@ -0,0 +1,53 @@ +package base + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// HandleGetSuccessfully creates an HTTP handler at `/` on the test handler mux +// that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", 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.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "resources": { + "rel/cdn": { + "href-template": "services{?marker,limit}", + "href-vars": { + "marker": "param/marker", + "limit": "param/limit" + }, + "hints": { + "allow": [ + "GET" + ], + "formats": { + "application/json": {} + } + } + } + } + } + `) + + }) +} + +// HandlePingSuccessfully creates an HTTP handler at `/ping` on the test handler +// mux that responds with a `Ping` response. +func HandlePingSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/requests.go new file mode 100644 index 00000000000..9d8632cbea6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/requests.go @@ -0,0 +1,30 @@ +package base + +import ( + "github.com/rackspace/gophercloud" + + "github.com/racker/perigee" +) + +// Get retrieves the home document, allowing the user to discover the +// entire API. +func Get(c *gophercloud.ServiceClient) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Ping retrieves a ping to the server. +func Ping(c *gophercloud.ServiceClient) PingResult { + var res PingResult + _, res.Err = perigee.Request("GET", pingURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{204}, + OmitAccept: true, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/requests_test.go new file mode 100644 index 00000000000..a8d95f9ace9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/requests_test.go @@ -0,0 +1,43 @@ +package base + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestGetHomeDocument(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := Get(fake.ServiceClient()).Extract() + th.CheckNoErr(t, err) + + expected := HomeDocument{ + "rel/cdn": map[string]interface{}{ + "href-template": "services{?marker,limit}", + "href-vars": map[string]interface{}{ + "marker": "param/marker", + "limit": "param/limit", + }, + "hints": map[string]interface{}{ + "allow": []string{"GET"}, + "formats": map[string]interface{}{ + "application/json": map[string]interface{}{}, + }, + }, + }, + } + th.CheckDeepEquals(t, expected, *actual) +} + +func TestPing(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePingSuccessfully(t) + + err := Ping(fake.ServiceClient()).ExtractErr() + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/results.go new file mode 100644 index 00000000000..bef1da8a175 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/results.go @@ -0,0 +1,35 @@ +package base + +import ( + "errors" + + "github.com/rackspace/gophercloud" +) + +// HomeDocument is a resource that contains all the resources for the CDN API. +type HomeDocument map[string]interface{} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a home document resource. +func (r GetResult) Extract() (*HomeDocument, error) { + if r.Err != nil { + return nil, r.Err + } + + submap, ok := r.Body.(map[string]interface{})["resources"] + if !ok { + return nil, errors.New("Unexpected HomeDocument structure") + } + casted := HomeDocument(submap.(map[string]interface{})) + + return &casted, nil +} + +// PingResult represents the result of a Ping operation. +type PingResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/urls.go new file mode 100644 index 00000000000..a95e18bca93 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/urls.go @@ -0,0 +1,11 @@ +package base + +import "github.com/rackspace/gophercloud" + +func getURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL() +} + +func pingURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ping") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/doc.go new file mode 100644 index 00000000000..d4066985cb2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/doc.go @@ -0,0 +1,6 @@ +// Package flavors provides information and interaction with the flavors API +// resource in the OpenStack CDN service. This API resource allows for +// listing flavors and retrieving a specific flavor. +// +// A flavor is a mapping configuration to a CDN provider. +package flavors diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/fixtures.go new file mode 100644 index 00000000000..f413b6bc41b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/fixtures.go @@ -0,0 +1,82 @@ +package flavors + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// HandleListCDNFlavorsSuccessfully creates an HTTP handler at `/flavors` on the test handler mux +// that responds with a `List` response. +func HandleListCDNFlavorsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/flavors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "flavors": [ + { + "id": "europe", + "providers": [ + { + "provider": "Fastly", + "links": [ + { + "href": "http://www.fastly.com", + "rel": "provider_url" + } + ] + } + ], + "links": [ + { + "href": "https://www.poppycdn.io/v1.0/flavors/europe", + "rel": "self" + } + ] + } + ] + } + `) + }) +} + +// HandleGetCDNFlavorSuccessfully creates an HTTP handler at `/flavors/{id}` on the test handler mux +// that responds with a `Get` response. +func HandleGetCDNFlavorSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/flavors/asia", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "id" : "asia", + "providers" : [ + { + "provider" : "ChinaCache", + "links": [ + { + "href": "http://www.chinacache.com", + "rel": "provider_url" + } + ] + } + ], + "links": [ + { + "href": "https://www.poppycdn.io/v1.0/flavors/asia", + "rel": "self" + } + ] + } + `) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/requests.go new file mode 100644 index 00000000000..88ac891e8e6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/requests.go @@ -0,0 +1,27 @@ +package flavors + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a single page of CDN flavors. +func List(c *gophercloud.ServiceClient) pagination.Pager { + url := listURL(c) + createPage := func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} + +// Get retrieves a specific flavor based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/requests_test.go new file mode 100644 index 00000000000..7ddf1b1c660 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/requests_test.go @@ -0,0 +1,90 @@ +package flavors + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleListCDNFlavorsSuccessfully(t) + + count := 0 + + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractFlavors(page) + if err != nil { + t.Errorf("Failed to extract flavors: %v", err) + return false, err + } + + expected := []Flavor{ + Flavor{ + ID: "europe", + Providers: []Provider{ + Provider{ + Provider: "Fastly", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://www.fastly.com", + Rel: "provider_url", + }, + }, + }, + }, + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/europe", + Rel: "self", + }, + }, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetCDNFlavorSuccessfully(t) + + expected := &Flavor{ + ID: "asia", + Providers: []Provider{ + Provider{ + Provider: "ChinaCache", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://www.chinacache.com", + Rel: "provider_url", + }, + }, + }, + }, + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/asia", + Rel: "self", + }, + }, + } + + + actual, err := Get(fake.ServiceClient(), "asia").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/results.go new file mode 100644 index 00000000000..8cab48b5367 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/results.go @@ -0,0 +1,71 @@ +package flavors + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Provider represents a provider for a particular flavor. +type Provider struct { + // Specifies the name of the provider. The name must not exceed 64 bytes in + // length and is limited to unicode, digits, underscores, and hyphens. + Provider string `mapstructure:"provider"` + // Specifies a list with an href where rel is provider_url. + Links []gophercloud.Link `mapstructure:"links"` +} + +// Flavor represents a mapping configuration to a CDN provider. +type Flavor struct { + // Specifies the name of the flavor. The name must not exceed 64 bytes in + // length and is limited to unicode, digits, underscores, and hyphens. + ID string `mapstructure:"id"` + // Specifies the list of providers mapped to this flavor. + Providers []Provider `mapstructure:"providers"` + // Specifies the self-navigating JSON document paths. + Links []gophercloud.Link `mapstructure:"links"` +} + +// FlavorPage is the page returned by a pager when traversing over a +// collection of CDN flavors. +type FlavorPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a FlavorPage contains no Flavors. +func (r FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(r) + if err != nil { + return true, err + } + return len(flavors) == 0, nil +} + +// ExtractFlavors extracts and returns Flavors. It is used while iterating over +// a flavors.List call. +func ExtractFlavors(page pagination.Page) ([]Flavor, error) { + var response struct { + Flavors []Flavor `json:"flavors"` + } + + err := mapstructure.Decode(page.(FlavorPage).Body, &response) + return response.Flavors, err +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that extracts a flavor from a GetResult. +func (r GetResult) Extract() (*Flavor, error) { + if r.Err != nil { + return nil, r.Err + } + + var res Flavor + + err := mapstructure.Decode(r.Body, &res) + + return &res, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/urls.go new file mode 100644 index 00000000000..6eb38d2939f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/urls.go @@ -0,0 +1,11 @@ +package flavors + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("flavors") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("flavors", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/doc.go new file mode 100644 index 00000000000..ceecaa5a5e4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/doc.go @@ -0,0 +1,7 @@ +// Package serviceassets provides information and interaction with the +// serviceassets API resource in the OpenStack CDN service. This API resource +// allows for deleting cached assets. +// +// A service distributes assets across the network. Service assets let you +// interrogate properties about these assets and perform certain actions on them. +package serviceassets diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/fixtures.go new file mode 100644 index 00000000000..38e7fc55f8d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/fixtures.go @@ -0,0 +1,19 @@ +package serviceassets + +import ( + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// HandleDeleteCDNAssetSuccessfully creates an HTTP handler at `/services/{id}/assets` on the test handler mux +// that responds with a `Delete` response. +func HandleDeleteCDNAssetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0/assets", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/requests.go new file mode 100644 index 00000000000..5248ef20c12 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/requests.go @@ -0,0 +1,52 @@ +package serviceassets + +import ( + "strings" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +// DeleteOptsBuilder allows extensions to add additional parameters to the Delete +// request. +type DeleteOptsBuilder interface { + ToCDNAssetDeleteParams() (string, error) +} + +// DeleteOpts is a structure that holds options for deleting CDN service assets. +type DeleteOpts struct { + // If all is set to true, specifies that the delete occurs against all of the + // assets for the service. + All bool `q:"all"` + // Specifies the relative URL of the asset to be deleted. + URL string `q:"url"` +} + +// ToCDNAssetDeleteParams formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToCDNAssetDeleteParams() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// Delete accepts a unique service ID or URL and deletes the CDN service asset associated with +// it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and +// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" +// are valid options for idOrURL. +func Delete(c *gophercloud.ServiceClient, idOrURL string, opts DeleteOptsBuilder) DeleteResult { + var url string + if strings.Contains(idOrURL, "/") { + url = idOrURL + } else { + url = deleteURL(c, idOrURL) + } + + var res DeleteResult + _, res.Err = perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/requests_test.go new file mode 100644 index 00000000000..32896eef7a8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/requests_test.go @@ -0,0 +1,18 @@ +package serviceassets + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleDeleteCDNAssetSuccessfully(t) + + err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/results.go new file mode 100644 index 00000000000..1d8734b7c32 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/results.go @@ -0,0 +1,8 @@ +package serviceassets + +import "github.com/rackspace/gophercloud" + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/urls.go new file mode 100644 index 00000000000..cb0aea8fca5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/urls.go @@ -0,0 +1,7 @@ +package serviceassets + +import "github.com/rackspace/gophercloud" + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("services", id, "assets") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/doc.go new file mode 100644 index 00000000000..41f7c60dae2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/doc.go @@ -0,0 +1,7 @@ +// Package services provides information and interaction with the services API +// resource in the OpenStack CDN service. This API resource allows for +// listing, creating, updating, retrieving, and deleting services. +// +// A service represents an application that has its content cached to the edge +// nodes. +package services diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/errors.go new file mode 100644 index 00000000000..359584c2a66 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/errors.go @@ -0,0 +1,7 @@ +package services + +import "fmt" + +func no(str string) error { + return fmt.Errorf("Required parameter %s not provided", str) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/fixtures.go new file mode 100644 index 00000000000..d9bc9f20b77 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/fixtures.go @@ -0,0 +1,372 @@ +package services + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// HandleListCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux +// that responds with a `List` response. +func HandleListCDNServiceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "links": [ + { + "rel": "next", + "href": "https://www.poppycdn.io/v1.0/services?marker=96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0&limit=20" + } + ], + "services": [ + { + "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + "name": "mywebsite.com", + "domains": [ + { + "domain": "www.mywebsite.com" + } + ], + "origins": [ + { + "origin": "mywebsite.com", + "port": 80, + "ssl": false + } + ], + "caching": [ + { + "name": "default", + "ttl": 3600 + }, + { + "name": "home", + "ttl": 17200, + "rules": [ + { + "name": "index", + "request_url": "/index.htm" + } + ] + }, + { + "name": "images", + "ttl": 12800, + "rules": [ + { + "name": "images", + "request_url": "*.png" + } + ] + } + ], + "restrictions": [ + { + "name": "website only", + "rules": [ + { + "name": "mywebsite.com", + "referrer": "www.mywebsite.com" + } + ] + } + ], + "flavor_id": "asia", + "status": "deployed", + "errors" : [], + "links": [ + { + "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + "rel": "self" + }, + { + "href": "mywebsite.com.cdn123.poppycdn.net", + "rel": "access_url" + }, + { + "href": "https://www.poppycdn.io/v1.0/flavors/asia", + "rel": "flavor" + } + ] + }, + { + "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1", + "name": "myothersite.com", + "domains": [ + { + "domain": "www.myothersite.com" + } + ], + "origins": [ + { + "origin": "44.33.22.11", + "port": 80, + "ssl": false + }, + { + "origin": "77.66.55.44", + "port": 80, + "ssl": false, + "rules": [ + { + "name": "videos", + "request_url": "^/videos/*.m3u" + } + ] + } + ], + "caching": [ + { + "name": "default", + "ttl": 3600 + } + ], + "restrictions": [ + {} + ], + "flavor_id": "europe", + "status": "deployed", + "links": [ + { + "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1", + "rel": "self" + }, + { + "href": "myothersite.com.poppycdn.net", + "rel": "access_url" + }, + { + "href": "https://www.poppycdn.io/v1.0/flavors/europe", + "rel": "flavor" + } + ] + } + ] + } + `) + case "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1": + fmt.Fprintf(w, `{ + "services": [] + }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleCreateCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux +// that responds with a `Create` response. +func HandleCreateCDNServiceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services", 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, ` + { + "name": "mywebsite.com", + "domains": [ + { + "domain": "www.mywebsite.com" + }, + { + "domain": "blog.mywebsite.com" + } + ], + "origins": [ + { + "origin": "mywebsite.com", + "port": 80, + "ssl": false + } + ], + "restrictions": [ + { + "name": "website only", + "rules": [ + { + "name": "mywebsite.com", + "referrer": "www.mywebsite.com" + } + ] + } + ], + "caching": [ + { + "name": "default", + "ttl": 3600 + } + ], + + "flavor_id": "cdn" + } + `) + w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleGetCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux +// that responds with a `Get` response. +func HandleGetCDNServiceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + "name": "mywebsite.com", + "domains": [ + { + "domain": "www.mywebsite.com", + "protocol": "http" + } + ], + "origins": [ + { + "origin": "mywebsite.com", + "port": 80, + "ssl": false + } + ], + "caching": [ + { + "name": "default", + "ttl": 3600 + }, + { + "name": "home", + "ttl": 17200, + "rules": [ + { + "name": "index", + "request_url": "/index.htm" + } + ] + }, + { + "name": "images", + "ttl": 12800, + "rules": [ + { + "name": "images", + "request_url": "*.png" + } + ] + } + ], + "restrictions": [ + { + "name": "website only", + "rules": [ + { + "name": "mywebsite.com", + "referrer": "www.mywebsite.com" + } + ] + } + ], + "flavor_id": "cdn", + "status": "deployed", + "errors" : [], + "links": [ + { + "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + "rel": "self" + }, + { + "href": "blog.mywebsite.com.cdn1.raxcdn.com", + "rel": "access_url" + }, + { + "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn", + "rel": "flavor" + } + ] + } + `) + }) +} + +// HandleUpdateCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux +// that responds with a `Update` response. +func HandleUpdateCDNServiceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestJSONRequest(t, r, ` + [ + { + "op": "add", + "path": "/domains/-", + "value": {"domain": "appended.mocksite4.com"} + }, + { + "op": "add", + "path": "/domains/4", + "value": {"domain": "inserted.mocksite4.com"} + }, + { + "op": "add", + "path": "/domains", + "value": [ + {"domain": "bulkadded1.mocksite4.com"}, + {"domain": "bulkadded2.mocksite4.com"} + ] + }, + { + "op": "replace", + "path": "/origins/2", + "value": {"origin": "44.33.22.11", "port": 80, "ssl": false} + }, + { + "op": "replace", + "path": "/origins", + "value": [ + {"origin": "44.33.22.11", "port": 80, "ssl": false}, + {"origin": "55.44.33.22", "port": 443, "ssl": true} + ] + }, + { + "op": "remove", + "path": "/caching/8" + }, + { + "op": "remove", + "path": "/caching" + }, + { + "op": "replace", + "path": "/name", + "value": "differentServiceName" + } + ] + `) + w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleDeleteCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux +// that responds with a `Delete` response. +func HandleDeleteCDNServiceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/requests.go new file mode 100644 index 00000000000..ad1e1da462d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/requests.go @@ -0,0 +1,391 @@ +package services + +import ( + "fmt" + "strings" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToCDNServiceListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Marker and Limit are used for pagination. +type ListOpts struct { + Marker string `q:"marker"` + Limit int `q:"limit"` +} + +// ToCDNServiceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToCDNServiceListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// CDN services. It accepts a ListOpts struct, which allows for pagination via +// marker and limit. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToCDNServiceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPage := func(r pagination.PageResult) pagination.Page { + p := ServicePage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + pager := pagination.NewPager(c, url, createPage) + return pager +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToCDNServiceCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // REQUIRED. Specifies the name of the service. The minimum length for name is + // 3. The maximum length is 256. + Name string + // REQUIRED. Specifies a list of domains used by users to access their website. + Domains []Domain + // REQUIRED. Specifies a list of origin domains or IP addresses where the + // original assets are stored. + Origins []Origin + // REQUIRED. Specifies the CDN provider flavor ID to use. For a list of + // flavors, see the operation to list the available flavors. The minimum + // length for flavor_id is 1. The maximum length is 256. + FlavorID string + // OPTIONAL. Specifies the TTL rules for the assets under this service. Supports wildcards for fine-grained control. + Caching []CacheRule + // OPTIONAL. Specifies the restrictions that define who can access assets (content from the CDN cache). + Restrictions []Restriction +} + +// ToCDNServiceCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToCDNServiceCreateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.Name == "" { + return nil, no("Name") + } + s["name"] = opts.Name + + if opts.Domains == nil { + return nil, no("Domains") + } + for _, domain := range opts.Domains { + if domain.Domain == "" { + return nil, no("Domains[].Domain") + } + } + s["domains"] = opts.Domains + + if opts.Origins == nil { + return nil, no("Origins") + } + for _, origin := range opts.Origins { + if origin.Origin == "" { + return nil, no("Origins[].Origin") + } + if origin.Rules == nil && len(opts.Origins) > 1 { + return nil, no("Origins[].Rules") + } + for _, rule := range origin.Rules { + if rule.Name == "" { + return nil, no("Origins[].Rules[].Name") + } + if rule.RequestURL == "" { + return nil, no("Origins[].Rules[].RequestURL") + } + } + } + s["origins"] = opts.Origins + + if opts.FlavorID == "" { + return nil, no("FlavorID") + } + s["flavor_id"] = opts.FlavorID + + if opts.Caching != nil { + for _, cache := range opts.Caching { + if cache.Name == "" { + return nil, no("Caching[].Name") + } + if cache.Rules != nil { + for _, rule := range cache.Rules { + if rule.Name == "" { + return nil, no("Caching[].Rules[].Name") + } + if rule.RequestURL == "" { + return nil, no("Caching[].Rules[].RequestURL") + } + } + } + } + s["caching"] = opts.Caching + } + + if opts.Restrictions != nil { + for _, restriction := range opts.Restrictions { + if restriction.Name == "" { + return nil, no("Restrictions[].Name") + } + if restriction.Rules != nil { + for _, rule := range restriction.Rules { + if rule.Name == "" { + return nil, no("Restrictions[].Rules[].Name") + } + } + } + } + s["restrictions"] = opts.Restrictions + } + + return s, nil +} + +// Create accepts a CreateOpts struct and creates a new CDN service using the +// values provided. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToCDNServiceCreateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + resp, err := perigee.Request("POST", createURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// Get retrieves a specific service based on its URL or its unique ID. For +// example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and +// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" +// are valid options for idOrURL. +func Get(c *gophercloud.ServiceClient, idOrURL string) GetResult { + var url string + if strings.Contains(idOrURL, "/") { + url = idOrURL + } else { + url = getURL(c, idOrURL) + } + + var res GetResult + _, res.Err = perigee.Request("GET", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + return res +} + +// Path is a JSON pointer location that indicates which service parameter is being added, replaced, +// or removed. +type Path struct { + baseElement string +} + +func (p Path) renderRoot() string { + return "/" + p.baseElement +} + +func (p Path) renderDash() string { + return fmt.Sprintf("/%s/-", p.baseElement) +} + +func (p Path) renderIndex(index int64) string { + return fmt.Sprintf("/%s/%d", p.baseElement, index) +} + +var ( + // PathDomains indicates that an update operation is to be performed on a Domain. + PathDomains = Path{baseElement: "domains"} + + // PathOrigins indicates that an update operation is to be performed on an Origin. + PathOrigins = Path{baseElement: "origins"} + + // PathCaching indicates that an update operation is to be performed on a CacheRule. + PathCaching = Path{baseElement: "caching"} +) + +type value interface { + toPatchValue() interface{} + appropriatePath() Path + renderRootOr(func(p Path) string) string +} + +// Patch represents a single update to an existing Service. Multiple updates to a service can be +// submitted at the same time. +type Patch interface { + ToCDNServiceUpdateMap() map[string]interface{} +} + +// Insertion is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to +// a Service at a fixed index. Use an Append instead to append the new value to the end of its +// collection. Pass it to the Update function as part of the Patch slice. +type Insertion struct { + Index int64 + Value value +} + +// ToCDNServiceUpdateMap converts an Insertion into a request body fragment suitable for the +// Update call. +func (i Insertion) ToCDNServiceUpdateMap() map[string]interface{} { + return map[string]interface{}{ + "op": "add", + "path": i.Value.renderRootOr(func(p Path) string { return p.renderIndex(i.Index) }), + "value": i.Value.toPatchValue(), + } +} + +// Append is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to a +// Service at the end of its respective collection. Use an Insertion instead to insert the value +// at a fixed index within the collection. Pass this to the Update function as part of its +// Patch slice. +type Append struct { + Value value +} + +// ToCDNServiceUpdateMap converts an Append into a request body fragment suitable for the +// Update call. +func (a Append) ToCDNServiceUpdateMap() map[string]interface{} { + return map[string]interface{}{ + "op": "add", + "path": a.Value.renderRootOr(func(p Path) string { return p.renderDash() }), + "value": a.Value.toPatchValue(), + } +} + +// Replacement is a Patch that alters a specific service parameter (Domain, Origin, or CacheRule) +// in-place by index. Pass it to the Update function as part of the Patch slice. +type Replacement struct { + Value value + Index int64 +} + +// ToCDNServiceUpdateMap converts a Replacement into a request body fragment suitable for the +// Update call. +func (r Replacement) ToCDNServiceUpdateMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": r.Value.renderRootOr(func(p Path) string { return p.renderIndex(r.Index) }), + "value": r.Value.toPatchValue(), + } +} + +// NameReplacement specifically updates the Service name. Pass it to the Update function as part +// of the Patch slice. +type NameReplacement struct { + NewName string +} + +// ToCDNServiceUpdateMap converts a NameReplacement into a request body fragment suitable for the +// Update call. +func (r NameReplacement) ToCDNServiceUpdateMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/name", + "value": r.NewName, + } +} + +// Removal is a Patch that requests the removal of a service parameter (Domain, Origin, or +// CacheRule) by index. Pass it to the Update function as part of the Patch slice. +type Removal struct { + Path Path + Index int64 + All bool +} + +// ToCDNServiceUpdateMap converts a Removal into a request body fragment suitable for the +// Update call. +func (r Removal) ToCDNServiceUpdateMap() map[string]interface{} { + result := map[string]interface{}{"op": "remove"} + if r.All { + result["path"] = r.Path.renderRoot() + } else { + result["path"] = r.Path.renderIndex(r.Index) + } + return result +} + +type UpdateOpts []Patch + +// Update accepts a slice of Patch operations (Insertion, Append, Replacement or Removal) and +// updates an existing CDN service using the values provided. idOrURL can be either the service's +// URL or its ID. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and +// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" +// are valid options for idOrURL. +func Update(c *gophercloud.ServiceClient, idOrURL string, opts UpdateOpts) UpdateResult { + var url string + if strings.Contains(idOrURL, "/") { + url = idOrURL + } else { + url = updateURL(c, idOrURL) + } + + reqBody := make([]map[string]interface{}, len(opts)) + for i, patch := range opts { + reqBody[i] = patch.ToCDNServiceUpdateMap() + } + + resp, err := perigee.Request("PATCH", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + var result UpdateResult + result.Header = resp.HttpResponse.Header + result.Err = err + return result +} + +// Delete accepts a service's ID or its URL and deletes the CDN service +// associated with it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and +// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" +// are valid options for idOrURL. +func Delete(c *gophercloud.ServiceClient, idOrURL string) DeleteResult { + var url string + if strings.Contains(idOrURL, "/") { + url = idOrURL + } else { + url = deleteURL(c, idOrURL) + } + + var res DeleteResult + _, res.Err = perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/requests_test.go new file mode 100644 index 00000000000..59e826f048d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/requests_test.go @@ -0,0 +1,358 @@ +package services + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleListCDNServiceSuccessfully(t) + + count := 0 + + err := List(fake.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractServices(page) + if err != nil { + t.Errorf("Failed to extract services: %v", err) + return false, err + } + + expected := []Service{ + Service{ + ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Name: "mywebsite.com", + Domains: []Domain{ + Domain{ + Domain: "www.mywebsite.com", + }, + }, + Origins: []Origin{ + Origin{ + Origin: "mywebsite.com", + Port: 80, + SSL: false, + }, + }, + Caching: []CacheRule{ + CacheRule{ + Name: "default", + TTL: 3600, + }, + CacheRule{ + Name: "home", + TTL: 17200, + Rules: []TTLRule{ + TTLRule{ + Name: "index", + RequestURL: "/index.htm", + }, + }, + }, + CacheRule{ + Name: "images", + TTL: 12800, + Rules: []TTLRule{ + TTLRule{ + Name: "images", + RequestURL: "*.png", + }, + }, + }, + }, + Restrictions: []Restriction{ + Restriction{ + Name: "website only", + Rules: []RestrictionRule{ + RestrictionRule{ + Name: "mywebsite.com", + Referrer: "www.mywebsite.com", + }, + }, + }, + }, + FlavorID: "asia", + Status: "deployed", + Errors: []Error{}, + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Rel: "self", + }, + gophercloud.Link{ + Href: "mywebsite.com.cdn123.poppycdn.net", + Rel: "access_url", + }, + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/asia", + Rel: "flavor", + }, + }, + }, + Service{ + ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1", + Name: "myothersite.com", + Domains: []Domain{ + Domain{ + Domain: "www.myothersite.com", + }, + }, + Origins: []Origin{ + Origin{ + Origin: "44.33.22.11", + Port: 80, + SSL: false, + }, + Origin{ + Origin: "77.66.55.44", + Port: 80, + SSL: false, + Rules: []OriginRule{ + OriginRule{ + Name: "videos", + RequestURL: "^/videos/*.m3u", + }, + }, + }, + }, + Caching: []CacheRule{ + CacheRule{ + Name: "default", + TTL: 3600, + }, + }, + Restrictions: []Restriction{}, + FlavorID: "europe", + Status: "deployed", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1", + Rel: "self", + }, + gophercloud.Link{ + Href: "myothersite.com.poppycdn.net", + Rel: "access_url", + }, + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/europe", + Rel: "flavor", + }, + }, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleCreateCDNServiceSuccessfully(t) + + createOpts := CreateOpts{ + Name: "mywebsite.com", + Domains: []Domain{ + Domain{ + Domain: "www.mywebsite.com", + }, + Domain{ + Domain: "blog.mywebsite.com", + }, + }, + Origins: []Origin{ + Origin{ + Origin: "mywebsite.com", + Port: 80, + SSL: false, + }, + }, + Restrictions: []Restriction{ + Restriction{ + Name: "website only", + Rules: []RestrictionRule{ + RestrictionRule{ + Name: "mywebsite.com", + Referrer: "www.mywebsite.com", + }, + }, + }, + }, + Caching: []CacheRule{ + CacheRule{ + Name: "default", + TTL: 3600, + }, + }, + FlavorID: "cdn", + } + + expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" + actual, err := Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, expected, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetCDNServiceSuccessfully(t) + + expected := &Service{ + ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Name: "mywebsite.com", + Domains: []Domain{ + Domain{ + Domain: "www.mywebsite.com", + Protocol: "http", + }, + }, + Origins: []Origin{ + Origin{ + Origin: "mywebsite.com", + Port: 80, + SSL: false, + }, + }, + Caching: []CacheRule{ + CacheRule{ + Name: "default", + TTL: 3600, + }, + CacheRule{ + Name: "home", + TTL: 17200, + Rules: []TTLRule{ + TTLRule{ + Name: "index", + RequestURL: "/index.htm", + }, + }, + }, + CacheRule{ + Name: "images", + TTL: 12800, + Rules: []TTLRule{ + TTLRule{ + Name: "images", + RequestURL: "*.png", + }, + }, + }, + }, + Restrictions: []Restriction{ + Restriction{ + Name: "website only", + Rules: []RestrictionRule{ + RestrictionRule{ + Name: "mywebsite.com", + Referrer: "www.mywebsite.com", + }, + }, + }, + }, + FlavorID: "cdn", + Status: "deployed", + Errors: []Error{}, + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Rel: "self", + }, + gophercloud.Link{ + Href: "blog.mywebsite.com.cdn1.raxcdn.com", + Rel: "access_url", + }, + gophercloud.Link{ + Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn", + Rel: "flavor", + }, + }, + } + + actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestSuccessfulUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleUpdateCDNServiceSuccessfully(t) + + expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" + ops := UpdateOpts{ + // Append a single Domain + Append{Value: Domain{Domain: "appended.mocksite4.com"}}, + // Insert a single Domain + Insertion{ + Index: 4, + Value: Domain{Domain: "inserted.mocksite4.com"}, + }, + // Bulk addition + Append{ + Value: DomainList{ + Domain{Domain: "bulkadded1.mocksite4.com"}, + Domain{Domain: "bulkadded2.mocksite4.com"}, + }, + }, + // Replace a single Origin + Replacement{ + Index: 2, + Value: Origin{Origin: "44.33.22.11", Port: 80, SSL: false}, + }, + // Bulk replace Origins + Replacement{ + Index: 0, // Ignored + Value: OriginList{ + Origin{Origin: "44.33.22.11", Port: 80, SSL: false}, + Origin{Origin: "55.44.33.22", Port: 443, SSL: true}, + }, + }, + // Remove a single CacheRule + Removal{ + Index: 8, + Path: PathCaching, + }, + // Bulk removal + Removal{ + All: true, + Path: PathCaching, + }, + // Service name replacement + NameReplacement{ + NewName: "differentServiceName", + }, + } + + actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, expected, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleDeleteCDNServiceSuccessfully(t) + + err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/results.go new file mode 100644 index 00000000000..33406c482b7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/results.go @@ -0,0 +1,316 @@ +package services + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Domain represents a domain used by users to access their website. +type Domain struct { + // Specifies the domain used to access the assets on their website, for which + // a CNAME is given to the CDN provider. + Domain string `mapstructure:"domain" json:"domain"` + // Specifies the protocol used to access the assets on this domain. Only "http" + // or "https" are currently allowed. The default is "http". + Protocol string `mapstructure:"protocol" json:"protocol,omitempty"` +} + +func (d Domain) toPatchValue() interface{} { + r := make(map[string]interface{}) + r["domain"] = d.Domain + if d.Protocol != "" { + r["protocol"] = d.Protocol + } + return r +} + +func (d Domain) appropriatePath() Path { + return PathDomains +} + +func (d Domain) renderRootOr(render func(p Path) string) string { + return render(d.appropriatePath()) +} + +// DomainList provides a useful way to perform bulk operations in a single Patch. +type DomainList []Domain + +func (list DomainList) toPatchValue() interface{} { + r := make([]interface{}, len(list)) + for i, domain := range list { + r[i] = domain.toPatchValue() + } + return r +} + +func (list DomainList) appropriatePath() Path { + return PathDomains +} + +func (list DomainList) renderRootOr(_ func(p Path) string) string { + return list.appropriatePath().renderRoot() +} + +// OriginRule represents a rule that defines when an origin should be accessed. +type OriginRule struct { + // Specifies the name of this rule. + Name string `mapstructure:"name" json:"name"` + // Specifies the request URL this rule should match for this origin to be used. Regex is supported. + RequestURL string `mapstructure:"request_url" json:"request_url"` +} + +// Origin specifies a list of origin domains or IP addresses where the original assets are stored. +type Origin struct { + // Specifies the URL or IP address to pull origin content from. + Origin string `mapstructure:"origin" json:"origin"` + // Specifies the port used to access the origin. The default is port 80. + Port int `mapstructure:"port" json:"port,omitempty"` + // Specifies whether or not to use HTTPS to access the origin. The default + // is false. + SSL bool `mapstructure:"ssl" json:"ssl"` + // Specifies a collection of rules that define the conditions when this origin + // should be accessed. If there is more than one origin, the rules parameter is required. + Rules []OriginRule `mapstructure:"rules" json:"rules,omitempty"` +} + +func (o Origin) toPatchValue() interface{} { + r := make(map[string]interface{}) + r["origin"] = o.Origin + r["port"] = o.Port + r["ssl"] = o.SSL + if len(o.Rules) > 0 { + r["rules"] = make([]map[string]interface{}, len(o.Rules)) + for index, rule := range o.Rules { + submap := r["rules"].([]map[string]interface{})[index] + submap["name"] = rule.Name + submap["request_url"] = rule.RequestURL + } + } + return r +} + +func (o Origin) appropriatePath() Path { + return PathOrigins +} + +func (o Origin) renderRootOr(render func(p Path) string) string { + return render(o.appropriatePath()) +} + +// OriginList provides a useful way to perform bulk operations in a single Patch. +type OriginList []Origin + +func (list OriginList) toPatchValue() interface{} { + r := make([]interface{}, len(list)) + for i, origin := range list { + r[i] = origin.toPatchValue() + } + return r +} + +func (list OriginList) appropriatePath() Path { + return PathOrigins +} + +func (list OriginList) renderRootOr(_ func(p Path) string) string { + return list.appropriatePath().renderRoot() +} + +// TTLRule specifies a rule that determines if a TTL should be applied to an asset. +type TTLRule struct { + // Specifies the name of this rule. + Name string `mapstructure:"name" json:"name"` + // Specifies the request URL this rule should match for this TTL to be used. Regex is supported. + RequestURL string `mapstructure:"request_url" json:"request_url"` +} + +// CacheRule specifies the TTL rules for the assets under this service. +type CacheRule struct { + // Specifies the name of this caching rule. Note: 'default' is a reserved name used for the default TTL setting. + Name string `mapstructure:"name" json:"name"` + // Specifies the TTL to apply. + TTL int `mapstructure:"ttl" json:"ttl"` + // Specifies a collection of rules that determine if this TTL should be applied to an asset. + Rules []TTLRule `mapstructure:"rules" json:"rules,omitempty"` +} + +func (c CacheRule) toPatchValue() interface{} { + r := make(map[string]interface{}) + r["name"] = c.Name + r["ttl"] = c.TTL + r["rules"] = make([]map[string]interface{}, len(c.Rules)) + for index, rule := range c.Rules { + submap := r["rules"].([]map[string]interface{})[index] + submap["name"] = rule.Name + submap["request_url"] = rule.RequestURL + } + return r +} + +func (c CacheRule) appropriatePath() Path { + return PathCaching +} + +func (c CacheRule) renderRootOr(render func(p Path) string) string { + return render(c.appropriatePath()) +} + +// CacheRuleList provides a useful way to perform bulk operations in a single Patch. +type CacheRuleList []CacheRule + +func (list CacheRuleList) toPatchValue() interface{} { + r := make([]interface{}, len(list)) + for i, rule := range list { + r[i] = rule.toPatchValue() + } + return r +} + +func (list CacheRuleList) appropriatePath() Path { + return PathCaching +} + +func (list CacheRuleList) renderRootOr(_ func(p Path) string) string { + return list.appropriatePath().renderRoot() +} + +// RestrictionRule specifies a rule that determines if this restriction should be applied to an asset. +type RestrictionRule struct { + // Specifies the name of this rule. + Name string `mapstructure:"name" json:"name"` + // Specifies the http host that requests must come from. + Referrer string `mapstructure:"referrer" json:"referrer,omitempty"` +} + +// Restriction specifies a restriction that defines who can access assets (content from the CDN cache). +type Restriction struct { + // Specifies the name of this restriction. + Name string `mapstructure:"name" json:"name"` + // Specifies a collection of rules that determine if this TTL should be applied to an asset. + Rules []RestrictionRule `mapstructure:"rules" json:"rules"` +} + +// Error specifies an error that occurred during the previous service action. +type Error struct { + // Specifies an error message detailing why there is an error. + Message string `mapstructure:"message"` +} + +// Service represents a CDN service resource. +type Service struct { + // Specifies the service ID that represents distributed content. The value is + // a UUID, such as 96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0, that is generated by the server. + ID string `mapstructure:"id"` + // Specifies the name of the service. + Name string `mapstructure:"name"` + // Specifies a list of domains used by users to access their website. + Domains []Domain `mapstructure:"domains"` + // Specifies a list of origin domains or IP addresses where the original assets are stored. + Origins []Origin `mapstructure:"origins"` + // Specifies the TTL rules for the assets under this service. Supports wildcards for fine grained control. + Caching []CacheRule `mapstructure:"caching"` + // Specifies the restrictions that define who can access assets (content from the CDN cache). + Restrictions []Restriction `mapstructure:"restrictions" json:"restrictions,omitempty"` + // Specifies the CDN provider flavor ID to use. For a list of flavors, see the operation to list the available flavors. + FlavorID string `mapstructure:"flavor_id"` + // Specifies the current status of the service. + Status string `mapstructure:"status"` + // Specifies the list of errors that occurred during the previous service action. + Errors []Error `mapstructure:"errors"` + // Specifies the self-navigating JSON document paths. + Links []gophercloud.Link `mapstructure:"links"` +} + +// ServicePage is the page returned by a pager when traversing over a +// collection of CDN services. +type ServicePage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a ListResult contains no services. +func (r ServicePage) IsEmpty() (bool, error) { + services, err := ExtractServices(r) + if err != nil { + return true, err + } + return len(services) == 0, nil +} + +// LastMarker returns the last service in a ListResult. +func (r ServicePage) LastMarker() (string, error) { + services, err := ExtractServices(r) + if err != nil { + return "", err + } + if len(services) == 0 { + return "", nil + } + return (services[len(services)-1]).ID, nil +} + +// ExtractServices is a function that takes a ListResult and returns the services' information. +func ExtractServices(page pagination.Page) ([]Service, error) { + var response struct { + Services []Service `mapstructure:"services"` + } + + err := mapstructure.Decode(page.(ServicePage).Body, &response) + return response.Services, err +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + gophercloud.Result +} + +// Extract is a method that extracts the location of a newly created service. +func (r CreateResult) Extract() (string, error) { + if r.Err != nil { + return "", r.Err + } + if l, ok := r.Header["Location"]; ok && len(l) > 0 { + return l[0], nil + } + return "", nil +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that extracts a service from a GetResult. +func (r GetResult) Extract() (*Service, error) { + if r.Err != nil { + return nil, r.Err + } + + var res Service + + err := mapstructure.Decode(r.Body, &res) + + return &res, err +} + +// UpdateResult represents the result of a Update operation. +type UpdateResult struct { + gophercloud.Result +} + +// Extract is a method that extracts the location of an updated service. +func (r UpdateResult) Extract() (string, error) { + if r.Err != nil { + return "", r.Err + } + if l, ok := r.Header["Location"]; ok && len(l) > 0 { + return l[0], nil + } + return "", nil +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/urls.go new file mode 100644 index 00000000000..d953d4c1981 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/urls.go @@ -0,0 +1,23 @@ +package services + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("services") +} + +func createURL(c *gophercloud.ServiceClient) string { + return listURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("services", id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client.go index 99b3d466d39..9c12dcaae47 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client.go @@ -203,3 +203,14 @@ func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoi } return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil } + +// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1 +// CDN service. +func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("cdn") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/doc.go new file mode 100644 index 00000000000..2571a1a5a77 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/doc.go @@ -0,0 +1 @@ +package defsecrules diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/fixtures.go new file mode 100644 index 00000000000..c28e492d357 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/fixtures.go @@ -0,0 +1,108 @@ +package defsecrules + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +const rootPath = "/os-security-group-default-rules" + +func mockListRulesResponse(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, ` +{ + "security_group_default_rules": [ + { + "from_port": 80, + "id": "{ruleID}", + "ip_protocol": "TCP", + "ip_range": { + "cidr": "10.10.10.0/24" + }, + "to_port": 80 + } + ] +} + `) + }) +} + +func mockCreateRuleResponse(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_default_rule": { + "ip_protocol": "TCP", + "from_port": 80, + "to_port": 80, + "cidr": "10.10.12.0/24" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group_default_rule": { + "from_port": 80, + "id": "{ruleID}", + "ip_protocol": "TCP", + "ip_range": { + "cidr": "10.10.12.0/24" + }, + "to_port": 80 + } +} +`) + }) +} + +func mockGetRuleResponse(t *testing.T, ruleID string) { + url := rootPath + "/" + ruleID + 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_default_rule": { + "id": "{ruleID}", + "from_port": 80, + "to_port": 80, + "ip_protocol": "TCP", + "ip_range": { + "cidr": "10.10.12.0/24" + } + } +} + `) + }) +} + +func mockDeleteRuleResponse(t *testing.T, ruleID string) { + url := rootPath + "/" + 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.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go new file mode 100644 index 00000000000..7d19741e10c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go @@ -0,0 +1,111 @@ +package defsecrules + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List will return a collection of default rules. +func List(client *gophercloud.ServiceClient) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return DefaultRulePage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, rootURL(client), createPage) +} + +// CreateOpts represents the configuration for adding a new default rule. +type CreateOpts struct { + // Required - the lower bound of the port range that will be opened. + FromPort int `json:"from_port"` + + // Required - the upper bound of the port range that will be opened. + ToPort int `json:"to_port"` + + // Required - the protocol type that will be allowed, e.g. TCP. + IPProtocol string `json:"ip_protocol"` + + // 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"` +} + +// CreateOptsBuilder builds the create rule options into a serializable format. +type CreateOptsBuilder interface { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// ToRuleCreateMap builds the create rule options into a serializable format. +func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) { + rule := make(map[string]interface{}) + + if opts.FromPort == 0 { + return rule, errors.New("A FromPort must be set") + } + if opts.ToPort == 0 { + return rule, errors.New("A ToPort must be set") + } + if opts.IPProtocol == "" { + return rule, errors.New("A IPProtocol must be set") + } + if opts.CIDR == "" { + return rule, errors.New("A CIDR must be set") + } + + rule["from_port"] = opts.FromPort + rule["to_port"] = opts.ToPort + rule["ip_protocol"] = opts.IPProtocol + rule["cidr"] = opts.CIDR + + return map[string]interface{}{"security_group_default_rule": rule}, nil +} + +// Create is the operation responsible for creating a new default rule. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var result CreateResult + + reqBody, err := opts.ToRuleCreateMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("POST", rootURL(client), perigee.Options{ + Results: &result.Body, + ReqBody: &reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Get will return details for a particular default rule. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + + _, result.Err = perigee.Request("GET", resourceURL(client, id), perigee.Options{ + Results: &result.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Delete will permanently delete a default rule from the project. +func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult { + var result gophercloud.ErrResult + + _, result.Err = perigee.Request("DELETE", resourceURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests_test.go new file mode 100644 index 00000000000..d4ebe87c564 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests_test.go @@ -0,0 +1,100 @@ +package defsecrules + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ruleID = "{ruleID}" + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListRulesResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractDefaultRules(page) + th.AssertNoErr(t, err) + + expected := []DefaultRule{ + DefaultRule{ + FromPort: 80, + ID: ruleID, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "10.10.10.0/24"}, + ToPort: 80, + }, + } + + 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() + + mockCreateRuleResponse(t) + + opts := CreateOpts{ + IPProtocol: "TCP", + FromPort: 80, + ToPort: 80, + CIDR: "10.10.12.0/24", + } + + group, err := Create(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &DefaultRule{ + ID: ruleID, + FromPort: 80, + ToPort: 80, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "10.10.12.0/24"}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetRuleResponse(t, ruleID) + + group, err := Get(client.ServiceClient(), ruleID).Extract() + th.AssertNoErr(t, err) + + expected := &DefaultRule{ + ID: ruleID, + FromPort: 80, + ToPort: 80, + IPProtocol: "TCP", + IPRange: secgroups.IPRange{CIDR: "10.10.12.0/24"}, + } + + th.AssertDeepEquals(t, expected, group) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteRuleResponse(t, ruleID) + + err := Delete(client.ServiceClient(), ruleID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/results.go new file mode 100644 index 00000000000..e588d3e3276 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/results.go @@ -0,0 +1,69 @@ +package defsecrules + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/rackspace/gophercloud/pagination" +) + +// DefaultRule represents a default rule - which is identical to a +// normal security rule. +type DefaultRule secgroups.Rule + +// DefaultRulePage is a single page of a DefaultRule collection. +type DefaultRulePage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of default rules contains any results. +func (page DefaultRulePage) IsEmpty() (bool, error) { + users, err := ExtractDefaultRules(page) + if err != nil { + return false, err + } + return len(users) == 0, nil +} + +// ExtractDefaultRules returns a slice of DefaultRules contained in a single +// page of results. +func ExtractDefaultRules(page pagination.Page) ([]DefaultRule, error) { + casted := page.(DefaultRulePage).Body + var response struct { + Rules []DefaultRule `mapstructure:"security_group_default_rules"` + } + + err := mapstructure.WeakDecode(casted, &response) + + return response.Rules, err +} + +type commonResult struct { + gophercloud.Result +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// Extract will extract a DefaultRule struct from most responses. +func (r commonResult) Extract() (*DefaultRule, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Rule DefaultRule `mapstructure:"security_group_default_rule"` + } + + err := mapstructure.WeakDecode(r.Body, &response) + + return &response.Rule, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/urls.go new file mode 100644 index 00000000000..cc928ab8952 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/urls.go @@ -0,0 +1,13 @@ +package defsecrules + +import "github.com/rackspace/gophercloud" + +const rulepath = "os-security-group-default-rules" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rulepath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rulepath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go index 7d1a2ac8b0c..7b372a355fb 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go @@ -5,9 +5,34 @@ import ( "github.com/racker/perigee" "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" "github.com/rackspace/gophercloud/pagination" ) +// CreateOptsExt adds a KeyPair option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + KeyName string `json:"key_name,omitempty"` +} + +// ToServerCreateMap adds the key_name and, optionally, key_data options 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 +} + // List returns a Pager that allows you to iterate over a collection of KeyPairs. func List(client *gophercloud.ServiceClient) pagination.Pager { return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { @@ -21,7 +46,7 @@ type CreateOptsBuilder interface { ToKeyPairCreateMap() (map[string]interface{}, error) } -// CreateOpts species keypair creation or import parameters. +// CreateOpts specifies keypair creation or import parameters. type CreateOpts struct { // Name [required] is a friendly name to refer to this KeyPair in other services. Name string diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go new file mode 100644 index 00000000000..702f32c9854 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go @@ -0,0 +1 @@ +package secgroups diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go new file mode 100644 index 00000000000..ca76f686b8b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/fixtures.go @@ -0,0 +1,265 @@ +package secgroups + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/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("%s/servers/%s%s", rootPath, 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": 12345 + } +} + `) + }) +} + +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 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go new file mode 100644 index 00000000000..09503d715ec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go @@ -0,0 +1,298 @@ +package secgroups + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +func commonList(client *gophercloud.ServiceClient, url string) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return SecurityGroupPage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, url, createPage) +} + +// List will return a collection of all the security groups for a particular +// tenant. +func List(client *gophercloud.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 *gophercloud.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 { + // Required - the name of your security group. + Name string `json:"name"` + + // Required - the description of your security group. + Description string `json:"description"` +} + +// CreateOpts is the struct responsible for creating a security group. +type CreateOpts GroupOpts + +// CreateOptsBuilder builds the create options into a serializable format. +type CreateOptsBuilder interface { + ToSecGroupCreateMap() (map[string]interface{}, error) +} + +var ( + errName = errors.New("Name is a required field") + errDesc = errors.New("Description is a required field") +) + +// ToSecGroupCreateMap builds the create options into a serializable format. +func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) { + sg := make(map[string]interface{}) + + if opts.Name == "" { + return sg, errName + } + if opts.Description == "" { + return sg, errDesc + } + + sg["name"] = opts.Name + sg["description"] = opts.Description + + return map[string]interface{}{"security_group": sg}, nil +} + +// Create will create a new security group. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var result CreateResult + + reqBody, err := opts.ToSecGroupCreateMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("POST", rootURL(client), perigee.Options{ + Results: &result.Body, + ReqBody: &reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// UpdateOpts is the struct responsible for updating an existing security group. +type UpdateOpts GroupOpts + +// UpdateOptsBuilder builds the update options into a serializable format. +type UpdateOptsBuilder interface { + ToSecGroupUpdateMap() (map[string]interface{}, error) +} + +// ToSecGroupUpdateMap builds the update options into a serializable format. +func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) { + sg := make(map[string]interface{}) + + if opts.Name == "" { + return sg, errName + } + if opts.Description == "" { + return sg, errDesc + } + + sg["name"] = opts.Name + sg["description"] = opts.Description + + return map[string]interface{}{"security_group": sg}, nil +} + +// Update will modify the mutable properties of a security group, notably its +// name and description. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var result UpdateResult + + reqBody, err := opts.ToSecGroupUpdateMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("PUT", resourceURL(client, id), perigee.Options{ + Results: &result.Body, + ReqBody: &reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Get will return details for a particular security group. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + + _, result.Err = perigee.Request("GET", resourceURL(client, id), perigee.Options{ + Results: &result.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Delete will permanently delete a security group from the project. +func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult { + var result gophercloud.ErrResult + + _, result.Err = perigee.Request("DELETE", resourceURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return result +} + +// CreateRuleOpts represents the configuration for adding a new rule to an +// existing security group. +type CreateRuleOpts struct { + // Required - the ID of the group that this rule will be added to. + ParentGroupID string `json:"parent_group_id"` + + // Required - the lower bound of the port range that will be opened. + FromPort int `json:"from_port"` + + // Required - the upper bound of the port range that will be opened. + ToPort int `json:"to_port"` + + // Required - the protocol type that will be allowed, e.g. TCP. + IPProtocol string `json:"ip_protocol"` + + // 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"` + + // 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"` +} + +// CreateRuleOptsBuilder builds the create rule options into a serializable format. +type CreateRuleOptsBuilder interface { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// ToRuleCreateMap builds the create rule options into a serializable format. +func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) { + rule := make(map[string]interface{}) + + if opts.ParentGroupID == "" { + return rule, errors.New("A ParentGroupID must be set") + } + if opts.FromPort == 0 { + return rule, errors.New("A FromPort must be set") + } + if opts.ToPort == 0 { + return rule, errors.New("A ToPort must be set") + } + if opts.IPProtocol == "" { + return rule, errors.New("A IPProtocol must be set") + } + if opts.CIDR == "" && opts.FromGroupID == "" { + return rule, errors.New("A CIDR or FromGroupID must be set") + } + + rule["parent_group_id"] = opts.ParentGroupID + rule["from_port"] = opts.FromPort + rule["to_port"] = opts.ToPort + rule["ip_protocol"] = opts.IPProtocol + + if opts.CIDR != "" { + rule["cidr"] = opts.CIDR + } + if opts.FromGroupID != "" { + rule["from_group_id"] = opts.FromGroupID + } + + return map[string]interface{}{"security_group_rule": rule}, nil +} + +// 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 *gophercloud.ServiceClient, opts CreateRuleOptsBuilder) CreateRuleResult { + var result CreateRuleResult + + reqBody, err := opts.ToRuleCreateMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("POST", rootRuleURL(client), perigee.Options{ + Results: &result.Body, + ReqBody: &reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// DeleteRule will permanently delete a rule from a security group. +func DeleteRule(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult { + var result gophercloud.ErrResult + + _, result.Err = perigee.Request("DELETE", resourceRuleURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return result +} + +func actionMap(prefix, groupName string) map[string]map[string]string { + return map[string]map[string]string{ + prefix + "SecurityGroup": map[string]string{"name": groupName}, + } +} + +// AddServerToGroup will associate a server and a security group, enforcing the +// rules of the group on the server. +func AddServerToGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult { + var result gophercloud.ErrResult + + _, result.Err = perigee.Request("POST", serverActionURL(client, serverID), perigee.Options{ + Results: &result.Body, + ReqBody: actionMap("add", groupName), + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return result +} + +// RemoveServerFromGroup will disassociate a server from a security group. +func RemoveServerFromGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult { + var result gophercloud.ErrResult + + _, result.Err = perigee.Request("POST", serverActionURL(client, serverID), perigee.Options{ + Results: &result.Body, + ReqBody: actionMap("remove", groupName), + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests_test.go new file mode 100644 index 00000000000..4e21d5deaa1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests_test.go @@ -0,0 +1,248 @@ +package secgroups + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + serverID = "{serverID}" + groupID = "{groupID}" + ruleID = "{ruleID}" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListGroupsResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractSecurityGroups(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []SecurityGroup{ + SecurityGroup{ + ID: groupID, + Description: "default", + Name: "default", + Rules: []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 := ListByServer(client.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractSecurityGroups(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []SecurityGroup{ + SecurityGroup{ + ID: groupID, + Description: "default", + Name: "default", + Rules: []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 := CreateOpts{ + Name: "test", + Description: "something", + } + + group, err := Create(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &SecurityGroup{ + ID: groupID, + Name: "test", + Description: "something", + TenantID: "openstack", + Rules: []Rule{}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateGroupResponse(t, groupID) + + opts := UpdateOpts{ + Name: "new_name", + Description: "new_desc", + } + + group, err := Update(client.ServiceClient(), groupID, opts).Extract() + th.AssertNoErr(t, err) + + expected := &SecurityGroup{ + ID: groupID, + Name: "new_name", + Description: "something", + TenantID: "openstack", + Rules: []Rule{}, + } + th.AssertDeepEquals(t, expected, group) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetGroupsResponse(t, groupID) + + group, err := Get(client.ServiceClient(), groupID).Extract() + th.AssertNoErr(t, err) + + expected := &SecurityGroup{ + ID: groupID, + Description: "default", + Name: "default", + TenantID: "openstack", + Rules: []Rule{ + Rule{ + FromPort: 80, + ToPort: 85, + IPProtocol: "TCP", + IPRange: IPRange{CIDR: "0.0.0.0"}, + Group: 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 := Get(client.ServiceClient(), "12345").Extract() + th.AssertNoErr(t, err) + + expected := &SecurityGroup{ID: "12345"} + th.AssertDeepEquals(t, expected, group) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteGroupResponse(t, groupID) + + err := Delete(client.ServiceClient(), groupID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAddRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddRuleResponse(t) + + opts := CreateRuleOpts{ + ParentGroupID: groupID, + FromPort: 22, + ToPort: 22, + IPProtocol: "TCP", + CIDR: "0.0.0.0/0", + } + + rule, err := CreateRule(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &Rule{ + FromPort: 22, + ToPort: 22, + Group: Group{}, + IPProtocol: "TCP", + ParentGroupID: groupID, + IPRange: 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 := DeleteRule(client.ServiceClient(), ruleID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAddServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddServerToGroupResponse(t, serverID) + + err := AddServerToGroup(client.ServiceClient(), serverID, "test").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestRemoveServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockRemoveServerFromGroupResponse(t, serverID) + + err := RemoveServerFromGroup(client.ServiceClient(), serverID, "test").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/results.go new file mode 100644 index 00000000000..478c5dc0973 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/results.go @@ -0,0 +1,147 @@ +package secgroups + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/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 + + // The human-readable name of the group, which needs to be unique. + Name string + + // The human-readable description of the group. + Description string + + // The rules which determine how this security group operates. + Rules []Rule + + // The ID of the tenant to which this security group belongs. + TenantID string `mapstructure:"tenant_id"` +} + +// 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 + + // The lower bound of the port range which this security group should open up + FromPort int `mapstructure:"from_port"` + + // The upper bound of the port range which this security group should open up + ToPort int `mapstructure:"to_port"` + + // The IP protocol (e.g. TCP) which the security group accepts + IPProtocol string `mapstructure:"ip_protocol"` + + // The CIDR IP range whose traffic can be received + IPRange IPRange `mapstructure:"ip_range"` + + // The security group ID to which this rule belongs + ParentGroupID string `mapstructure:"parent_group_id"` + + // Not documented. + Group Group +} + +// 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 `mapstructure:"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) + if err != nil { + return false, err + } + return len(users) == 0, nil +} + +// ExtractSecurityGroups returns a slice of SecurityGroups contained in a single page of results. +func ExtractSecurityGroups(page pagination.Page) ([]SecurityGroup, error) { + casted := page.(SecurityGroupPage).Body + var response struct { + SecurityGroups []SecurityGroup `mapstructure:"security_groups"` + } + + err := mapstructure.WeakDecode(casted, &response) + + return response.SecurityGroups, err +} + +type commonResult struct { + gophercloud.Result +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// Extract will extract a SecurityGroup struct from most responses. +func (r commonResult) Extract() (*SecurityGroup, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + SecurityGroup SecurityGroup `mapstructure:"security_group"` + } + + err := mapstructure.WeakDecode(r.Body, &response) + + return &response.SecurityGroup, err +} + +// CreateRuleResult represents the result when adding rules to a security group. +type CreateRuleResult struct { + gophercloud.Result +} + +// Extract will extract a Rule struct from a CreateRuleResult. +func (r CreateRuleResult) Extract() (*Rule, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Rule Rule `mapstructure:"security_group_rule"` + } + + err := mapstructure.WeakDecode(r.Body, &response) + + return &response.Rule, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go new file mode 100644 index 00000000000..f4760b6f2e8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go @@ -0,0 +1,32 @@ +package secgroups + +import "github.com/rackspace/gophercloud" + +const ( + secgrouppath = "os-security-groups" + rulepath = "os-security-group-rules" +) + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(secgrouppath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(secgrouppath) +} + +func listByServerURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL(secgrouppath, "servers", serverID, secgrouppath) +} + +func rootRuleURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rulepath) +} + +func resourceRuleURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rulepath, id) +} + +func serverActionURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("servers", id, "action") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/doc.go new file mode 100644 index 00000000000..d2729f87437 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/doc.go @@ -0,0 +1,5 @@ +/* +Package startstop provides functionality to start and stop servers that have +been provisioned by the OpenStack Compute service. +*/ +package startstop diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/fixtures.go new file mode 100644 index 00000000000..670828a986d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/fixtures.go @@ -0,0 +1,27 @@ +package startstop + +import ( + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/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/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests.go new file mode 100644 index 00000000000..99c91b054a3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests.go @@ -0,0 +1,40 @@ +package startstop + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" +) + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +// Start is the operation responsible for starting a Compute server. +func Start(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult { + var res gophercloud.ErrResult + + reqBody := map[string]interface{}{"os-start": nil} + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// Stop is the operation responsible for stopping a Compute server. +func Stop(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult { + var res gophercloud.ErrResult + + reqBody := map[string]interface{}{"os-stop": nil} + + _, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: reqBody, + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests_test.go new file mode 100644 index 00000000000..97a121b19aa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests_test.go @@ -0,0 +1,30 @@ +package startstop + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const serverID = "{serverId}" + +func TestStart(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockStartServerResponse(t, serverID) + + err := Start(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestStop(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockStopServerResponse(t, serverID) + + err := Stop(client.ServiceClient(), serverID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go index e872b071774..01646058e6f 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go @@ -457,3 +457,103 @@ func HandleRebuildSuccessfully(t *testing.T, response string) { 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"}}`)) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go index 95a4188d5f2..be8ab73b75b 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go @@ -2,6 +2,7 @@ package servers import ( "encoding/base64" + "errors" "fmt" "github.com/racker/perigee" @@ -131,6 +132,16 @@ type CreateOpts struct { // ConfigDrive [optional] enables metadata injection through a configuration drive. ConfigDrive bool + + // AdminPass [optional] sets the root user password. If not set, a randomly-generated + // password will be created and returned in the response. + AdminPass string + + // AccessIPv4 [optional] specifies an IPv4 address for the instance. + AccessIPv4 string + + // AccessIPv6 [optional] specifies an IPv6 address for the instance. + AccessIPv6 string } // ToServerCreateMap assembles a request body based on the contents of a CreateOpts. @@ -158,12 +169,22 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { if opts.Metadata != nil { server["metadata"] = opts.Metadata } + if opts.AdminPass != "" { + server["adminPass"] = opts.AdminPass + } + if opts.AccessIPv4 != "" { + server["accessIPv4"] = opts.AccessIPv4 + } + if opts.AccessIPv6 != "" { + server["accessIPv6"] = opts.AccessIPv6 + } 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} } + server["security_groups"] = securityGroups } if len(opts.Networks) > 0 { @@ -221,6 +242,7 @@ func Get(client *gophercloud.ServiceClient, id string) GetResult { _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{ Results: &result.Body, MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200, 203}, }) return result } @@ -475,7 +497,7 @@ type ResizeOpts struct { FlavorRef string } -// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body to the +// 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) { resize := map[string]interface{}{ @@ -536,3 +558,182 @@ func RevertResize(client *gophercloud.ServiceClient, id string) ActionResult { return res } + +// 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 +} + +// 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) { + server := make(map[string]interface{}) + if opts.AdminPass != "" { + server["adminPass"] = opts.AdminPass + } + return map[string]interface{}{"rescue": server}, nil +} + +// Rescue instructs the provider to place the server into RESCUE mode. +func Rescue(client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) RescueResult { + var result RescueResult + + if id == "" { + result.Err = fmt.Errorf("ID is required") + return result + } + reqBody, err := opts.ToServerRescueMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{ + Results: &result.Body, + ReqBody: &reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// 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 *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) ResetMetadataResult { + var res ResetMetadataResult + metadata, err := opts.ToMetadataResetMap() + if err != nil { + res.Err = err + return res + } + _, res.Err = perigee.Request("PUT", metadataURL(client, id), perigee.Options{ + ReqBody: metadata, + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(client *gophercloud.ServiceClient, id string) GetMetadataResult { + var res GetMetadataResult + _, res.Err = perigee.Request("GET", metadataURL(client, id), perigee.Options{ + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} + +// 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 *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult { + var res UpdateMetadataResult + metadata, err := opts.ToMetadataUpdateMap() + if err != nil { + res.Err = err + return res + } + _, res.Err = perigee.Request("POST", metadataURL(client, id), perigee.Options{ + ReqBody: metadata, + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} + +// 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 { + return nil, "", errors.New("CreateMetadatum operation must have 1 and only 1 key-value pair.") + } + 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 *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) CreateMetadatumResult { + var res CreateMetadatumResult + metadatum, key, err := opts.ToMetadatumCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", metadatumURL(client, id, key), perigee.Options{ + ReqBody: metadatum, + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} + +// Metadatum requests the key-value pair with the given key for the given server ID. +func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumResult { + var res GetMetadatumResult + _, res.Err = perigee.Request("GET", metadatumURL(client, id, key), perigee.Options{ + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} + +// DeleteMetadatum will delete the key-value pair with the given key for the given server ID. +func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult { + var res DeleteMetadatumResult + _, res.Err = perigee.Request("DELETE", metadatumURL(client, id, key), perigee.Options{ + Results: &res.Body, + MoreHeaders: client.AuthenticatedHeaders(), + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go index 392e2d88d3d..017e793ccd2 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go @@ -174,3 +174,93 @@ func TestRevertResize(t *testing.T) { res := RevertResize(client.ServiceClient(), "1234asdf") th.AssertNoErr(t, res.Err) } + +func TestRescue(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleServerRescueSuccessfully(t) + + res := Rescue(client.ServiceClient(), "1234asdf", 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 := 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 := CreateMetadatum(client.ServiceClient(), "1234asdf", 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 := 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 := 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 := ResetMetadata(client.ServiceClient(), "1234asdf", 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 := UpdateMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{ + "foo": "baz", + "this": "those", + }).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go index 53946baf66d..d63d7c887aa 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go @@ -1,6 +1,8 @@ package servers import ( + "reflect" + "github.com/mitchellh/mapstructure" "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" @@ -20,8 +22,21 @@ func (r serverResult) Extract() (*Server, error) { Server Server `mapstructure:"server"` } - err := mapstructure.Decode(r.Body, &response) - return &response.Server, err + config := &mapstructure.DecoderConfig{ + DecodeHook: toMapFromString, + Result: &response, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return nil, err + } + + err = decoder.Decode(r.Body) + if err != nil { + return nil, err + } + + return &response.Server, nil } // CreateResult temporarily contains the response from a Create call. @@ -39,7 +54,7 @@ type UpdateResult struct { serverResult } -// DeleteResult temporarily contains the response from an Delete call. +// DeleteResult temporarily contains the response from a Delete call. type DeleteResult struct { gophercloud.ErrResult } @@ -54,6 +69,25 @@ type ActionResult struct { gophercloud.ErrResult } +// RescueResult represents the result of a server rescue operation +type RescueResult struct { + ActionResult +} + +// Extract interprets any RescueResult as an AdminPass, if possible. +func (r RescueResult) Extract() (string, error) { + if r.Err != nil { + return "", r.Err + } + + var response struct { + AdminPass string `mapstructure:"adminPass"` + } + + err := mapstructure.Decode(r.Body, &response) + return response.AdminPass, err +} + // 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. @@ -145,6 +179,93 @@ func ExtractServers(page pagination.Page) ([]Server, error) { var response struct { Servers []Server `mapstructure:"servers"` } - err := mapstructure.Decode(casted, &response) + + config := &mapstructure.DecoderConfig{ + DecodeHook: toMapFromString, + Result: &response, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return nil, err + } + + err = decoder.Decode(casted) + + //err := mapstructure.Decode(casted, &response) return response.Servers, err } + +// MetadataResult contains the result of a call for (potentially) multiple key-value pairs. +type MetadataResult struct { + gophercloud.Result +} + +// GetMetadataResult temporarily contains the response from a metadata Get call. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult temporarily contains the response from a metadata Reset call. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult temporarily contains the response from a metadata Update call. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single key-value pair. +type MetadatumResult struct { + gophercloud.Result +} + +// GetMetadatumResult temporarily contains the response from a metadatum Get call. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult temporarily contains the response from a metadatum Create call. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call. +type DeleteMetadatumResult struct { + gophercloud.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Metadata map[string]string `mapstructure:"metadata"` + } + + err := mapstructure.Decode(r.Body, &response) + return response.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Metadatum map[string]string `mapstructure:"meta"` + } + + err := mapstructure.Decode(r.Body, &response) + return response.Metadatum, err +} + +func toMapFromString(from reflect.Kind, to reflect.Kind, data interface{}) (interface{}, error) { + if (from == reflect.String) && (to == reflect.Map) { + return map[string]interface{}{}, nil + } + return data, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go index 57587abca73..4bc6586a507 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go @@ -29,3 +29,11 @@ func updateURL(client *gophercloud.ServiceClient, id string) string { func actionURL(client *gophercloud.ServiceClient, id string) string { return client.ServiceURL("servers", id, "action") } + +func metadatumURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("servers", id, "metadata", key) +} + +func metadataURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "metadata") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls_test.go index cc895c90cf2..17a1d287f25 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls_test.go @@ -54,3 +54,15 @@ func TestActionURL(t *testing.T) { expected := endpoint + "servers/foo/action" th.CheckEquals(t, expected, actual) } + +func TestMetadatumURL(t *testing.T) { + actual := metadatumURL(endpointClient(), "foo", "bar") + expected := endpoint + "servers/foo/metadata/bar" + th.CheckEquals(t, expected, actual) +} + +func TestMetadataURL(t *testing.T) { + actual := metadataURL(endpointClient(), "foo") + expected := endpoint + "servers/foo/metadata" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util_test.go deleted file mode 100644 index e192ae3bd9e..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package servers - -import ( - "fmt" - "net/http" - "testing" - "time" - - th "github.com/rackspace/gophercloud/testhelper" - "github.com/rackspace/gophercloud/testhelper/client" -) - -func TestWaitForStatus(t *testing.T) { - th.SetupHTTP() - defer th.TeardownHTTP() - - th.Mux.HandleFunc("/servers/4321", func(w http.ResponseWriter, r *http.Request) { - time.Sleep(2 * time.Second) - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, ` - { - "server": { - "name": "the-server", - "id": "4321", - "status": "ACTIVE" - } - }`) - }) - - err := WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 0) - if err == nil { - t.Errorf("Expected error: 'Time Out in WaitFor'") - } - - err = WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 3) - th.CheckNoErr(t, err) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/docs.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/docs.go new file mode 100644 index 00000000000..89541787167 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/docs.go @@ -0,0 +1,16 @@ +// Package roles provides functionality to interact with and control roles on +// the API. +// +// A role represents a personality that a user can assume when performing a +// specific set of operations. If a role includes a set of rights and +// privileges, a user assuming that role inherits those rights and privileges. +// +// When a token is generated, the list of roles that user can assume is returned +// back to them. Services that are being called by that user determine how they +// interpret the set of roles a user has and to which operations or resources +// each role grants access. +// +// It is up to individual services such as Compute or Image to assign meaning +// to these roles. As far as the Identity service is concerned, a role is an +// arbitrary name assigned by the user. +package roles diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/fixtures.go new file mode 100644 index 00000000000..8256f0fe8e6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/fixtures.go @@ -0,0 +1,48 @@ +package roles + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/OS-KSADM/roles", 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, ` +{ + "roles": [ + { + "id": "123", + "name": "compute:admin", + "description": "Nova Administrator" + } + ] +} + `) + }) +} + +func MockAddUserRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusCreated) + }) +} + +func MockDeleteUserRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", 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) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go new file mode 100644 index 00000000000..152031ac350 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go @@ -0,0 +1,44 @@ +package roles + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List is the operation responsible for listing all available global roles +// that a user can adopt. +func List(client *gophercloud.ServiceClient) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, rootURL(client), createPage) +} + +// AddUserRole is the operation responsible for assigning a particular role to +// a user. This is confined to the scope of the user's tenant - so the tenant +// ID is a required argument. +func AddUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult { + var result UserRoleResult + + _, result.Err = perigee.Request("PUT", userRoleURL(client, tenantID, userID, roleID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200, 201}, + }) + + return result +} + +// DeleteUserRole is the operation responsible for deleting a particular role +// from a user. This is confined to the scope of the user's tenant - so the +// tenant ID is a required argument. +func DeleteUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult { + var result UserRoleResult + + _, result.Err = perigee.Request("DELETE", userRoleURL(client, tenantID, userID, roleID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests_test.go new file mode 100644 index 00000000000..7bfeea44a81 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests_test.go @@ -0,0 +1,64 @@ +package roles + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListRoleResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractRoles(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []Role{ + Role{ + ID: "123", + Name: "compute:admin", + Description: "Nova Administrator", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestAddUserRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockAddUserRoleResponse(t) + + err := AddUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestDeleteUserRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteUserRoleResponse(t) + + err := DeleteUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/results.go new file mode 100644 index 00000000000..ebb3aa530b8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/results.go @@ -0,0 +1,53 @@ +package roles + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Role represents an API role resource. +type Role struct { + // The unique ID for the role. + ID string + + // The human-readable name of the role. + Name string + + // The description of the role. + Description string + + // The associated service for this role. + ServiceID string +} + +// RolePage is a single page of a user Role collection. +type RolePage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (page RolePage) IsEmpty() (bool, error) { + users, err := ExtractRoles(page) + if err != nil { + return false, err + } + return len(users) == 0, nil +} + +// ExtractRoles returns a slice of roles contained in a single page of results. +func ExtractRoles(page pagination.Page) ([]Role, error) { + casted := page.(RolePage).Body + var response struct { + Roles []Role `mapstructure:"roles"` + } + + err := mapstructure.Decode(casted, &response) + return response.Roles, err +} + +// UserRoleResult represents the result of either an AddUserRole or +// a DeleteUserRole operation. +type UserRoleResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/urls.go new file mode 100644 index 00000000000..61b31551dd9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/urls.go @@ -0,0 +1,21 @@ +package roles + +import "github.com/rackspace/gophercloud" + +const ( + ExtPath = "OS-KSADM" + RolePath = "roles" + UserPath = "users" +) + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(ExtPath, RolePath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(ExtPath, RolePath) +} + +func userRoleURL(c *gophercloud.ServiceClient, tenantID, userID, roleID string) string { + return c.ServiceURL("tenants", tenantID, UserPath, userID, RolePath, ExtPath, roleID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/doc.go new file mode 100644 index 00000000000..82abcb9fccb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/doc.go @@ -0,0 +1 @@ +package users diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/fixtures.go new file mode 100644 index 00000000000..8941868dd2b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/fixtures.go @@ -0,0 +1,163 @@ +package users + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users", 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, ` +{ + "users":[ + { + "id": "u1000", + "name": "John Smith", + "username": "jqsmith", + "email": "john.smith@example.org", + "enabled": true, + "tenant_id": "12345" + }, + { + "id": "u1001", + "name": "Jane Smith", + "username": "jqsmith", + "email": "jane.smith@example.org", + "enabled": true, + "tenant_id": "12345" + } + ] +} + `) + }) +} + +func mockCreateUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users", 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, ` +{ + "user": { + "name": "new_user", + "tenant_id": "12345", + "enabled": false, + "email": "new_user@foo.com" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "name": "new_user", + "tenant_id": "12345", + "enabled": false, + "email": "new_user@foo.com", + "id": "c39e3de9be2d4c779f1dfd6abacc176d" + } +} +`) + }) +} + +func mockGetUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users/new_user", 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, ` +{ + "user": { + "name": "new_user", + "tenant_id": "12345", + "enabled": false, + "email": "new_user@foo.com", + "id": "c39e3de9be2d4c779f1dfd6abacc176d" + } +} +`) + }) +} + +func mockUpdateUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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, ` +{ + "user": { + "name": "new_name", + "enabled": true, + "email": "new_email@foo.com" + } +} +`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "name": "new_name", + "tenant_id": "12345", + "enabled": true, + "email": "new_email@foo.com", + "id": "c39e3de9be2d4c779f1dfd6abacc176d" + } +} +`) + }) +} + +func mockDeleteUserResponse(t *testing.T) { + th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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) + }) +} + +func mockListRolesResponse(t *testing.T) { + th.Mux.HandleFunc("/tenants/1d8b6120dcc640fda4fc9194ffc80273/users/c39e3de9be2d4c779f1dfd6abacc176d/roles", 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, ` +{ + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "foo_role" + }, + { + "id": "1ea3d56793574b668e85960fbf651e13", + "name": "admin" + } + ] +} + `) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests.go new file mode 100644 index 00000000000..4ce395f2dd3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests.go @@ -0,0 +1,180 @@ +package users + +import ( + "errors" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +func List(client *gophercloud.ServiceClient) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return UserPage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, rootURL(client), createPage) +} + +// EnabledState represents whether the user is enabled or not. +type EnabledState *bool + +// Useful variables to use when creating or updating users. +var ( + iTrue = true + iFalse = false + + Enabled EnabledState = &iTrue + Disabled EnabledState = &iFalse +) + +// CommonOpts are the parameters that are shared between CreateOpts and +// UpdateOpts +type CommonOpts struct { + // Either a name or username is required. When provided, the value must be + // unique or a 409 conflict error will be returned. If you provide a name but + // omit a username, the latter will be set to the former; and vice versa. + Name, Username string + + // The ID of the tenant to which you want to assign this user. + TenantID string + + // Indicates whether this user is enabled or not. + Enabled EnabledState + + // The email address of this user. + Email string +} + +// CreateOpts represents the options needed when creating new users. +type CreateOpts CommonOpts + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +type CreateOptsBuilder interface { + ToUserCreateMap() (map[string]interface{}, error) +} + +// ToUserCreateMap assembles a request body based on the contents of a CreateOpts. +func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + m := make(map[string]interface{}) + + if opts.Name == "" && opts.Username == "" { + return m, errors.New("Either a Name or Username must be provided") + } + + if opts.Name != "" { + m["name"] = opts.Name + } + if opts.Username != "" { + m["username"] = opts.Username + } + if opts.Enabled != nil { + m["enabled"] = &opts.Enabled + } + if opts.Email != "" { + m["email"] = opts.Email + } + if opts.TenantID != "" { + m["tenant_id"] = opts.TenantID + } + + return map[string]interface{}{"user": m}, nil +} + +// Create is the operation responsible for creating new users. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToUserCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", rootURL(client), perigee.Options{ + Results: &res.Body, + ReqBody: reqBody, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200, 201}, + }) + + return res +} + +// Get requests details on a single user, either by ID. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + + _, result.Err = perigee.Request("GET", ResourceURL(client, id), perigee.Options{ + Results: &result.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToUserUpdateMap() map[string]interface{} +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts CommonOpts + +// ToUserUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} { + m := make(map[string]interface{}) + + if opts.Name != "" { + m["name"] = opts.Name + } + if opts.Username != "" { + m["username"] = opts.Username + } + if opts.Enabled != nil { + m["enabled"] = &opts.Enabled + } + if opts.Email != "" { + m["email"] = opts.Email + } + if opts.TenantID != "" { + m["tenant_id"] = opts.TenantID + } + + return map[string]interface{}{"user": m} +} + +// Update is the operation responsible for updating exist users by their UUID. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var result UpdateResult + + _, result.Err = perigee.Request("PUT", ResourceURL(client, id), perigee.Options{ + Results: &result.Body, + ReqBody: opts.ToUserUpdateMap(), + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Delete is the operation responsible for permanently deleting an API user. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var result DeleteResult + + _, result.Err = perigee.Request("DELETE", ResourceURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + + return result +} + +func ListRoles(client *gophercloud.ServiceClient, tenantID, userID string) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, listRolesURL(client, tenantID, userID), createPage) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests_test.go new file mode 100644 index 00000000000..04f837163ab --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests_test.go @@ -0,0 +1,165 @@ +package users + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListUserResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractUsers(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []User{ + User{ + ID: "u1000", + Name: "John Smith", + Username: "jqsmith", + Email: "john.smith@example.org", + Enabled: true, + TenantID: "12345", + }, + User{ + ID: "u1001", + Name: "Jane Smith", + Username: "jqsmith", + Email: "jane.smith@example.org", + Enabled: true, + TenantID: "12345", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateUserResponse(t) + + opts := CreateOpts{ + Name: "new_user", + TenantID: "12345", + Enabled: Disabled, + Email: "new_user@foo.com", + } + + user, err := Create(client.ServiceClient(), opts).Extract() + + th.AssertNoErr(t, err) + + expected := &User{ + Name: "new_user", + ID: "c39e3de9be2d4c779f1dfd6abacc176d", + Email: "new_user@foo.com", + Enabled: false, + TenantID: "12345", + } + + th.AssertDeepEquals(t, expected, user) +} + +func TestGetUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetUserResponse(t) + + user, err := Get(client.ServiceClient(), "new_user").Extract() + th.AssertNoErr(t, err) + + expected := &User{ + Name: "new_user", + ID: "c39e3de9be2d4c779f1dfd6abacc176d", + Email: "new_user@foo.com", + Enabled: false, + TenantID: "12345", + } + + th.AssertDeepEquals(t, expected, user) +} + +func TestUpdateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateUserResponse(t) + + id := "c39e3de9be2d4c779f1dfd6abacc176d" + opts := UpdateOpts{ + Name: "new_name", + Enabled: Enabled, + Email: "new_email@foo.com", + } + + user, err := Update(client.ServiceClient(), id, opts).Extract() + + th.AssertNoErr(t, err) + + expected := &User{ + Name: "new_name", + ID: id, + Email: "new_email@foo.com", + Enabled: true, + TenantID: "12345", + } + + th.AssertDeepEquals(t, expected, user) +} + +func TestDeleteUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteUserResponse(t) + + res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d") + th.AssertNoErr(t, res.Err) +} + +func TestListingUserRoles(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListRolesResponse(t) + + tenantID := "1d8b6120dcc640fda4fc9194ffc80273" + userID := "c39e3de9be2d4c779f1dfd6abacc176d" + + err := ListRoles(client.ServiceClient(), tenantID, userID).EachPage(func(page pagination.Page) (bool, error) { + actual, err := ExtractRoles(page) + th.AssertNoErr(t, err) + + expected := []Role{ + Role{ID: "9fe2ff9ee4384b1894a90878d3e92bab", Name: "foo_role"}, + Role{ID: "1ea3d56793574b668e85960fbf651e13", Name: "admin"}, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/results.go new file mode 100644 index 00000000000..f531d5d023a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/results.go @@ -0,0 +1,128 @@ +package users + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// User represents a user resource that exists on the API. +type User struct { + // The UUID for this user. + ID string + + // The human name for this user. + Name string + + // The username for this user. + Username string + + // Indicates whether the user is enabled (true) or disabled (false). + Enabled bool + + // The email address for this user. + Email string + + // The ID of the tenant to which this user belongs. + TenantID string `mapstructure:"tenant_id"` +} + +// Role assigns specific responsibilities to users, allowing them to accomplish +// certain API operations whilst scoped to a service. +type Role struct { + // UUID of the role + ID string + + // Name of the role + Name string +} + +// UserPage is a single page of a User collection. +type UserPage struct { + pagination.SinglePageBase +} + +// RolePage is a single page of a user Role collection. +type RolePage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (page UserPage) IsEmpty() (bool, error) { + users, err := ExtractUsers(page) + if err != nil { + return false, err + } + return len(users) == 0, nil +} + +// ExtractUsers returns a slice of Tenants contained in a single page of results. +func ExtractUsers(page pagination.Page) ([]User, error) { + casted := page.(UserPage).Body + var response struct { + Users []User `mapstructure:"users"` + } + + err := mapstructure.Decode(casted, &response) + return response.Users, err +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (page RolePage) IsEmpty() (bool, error) { + users, err := ExtractRoles(page) + if err != nil { + return false, err + } + return len(users) == 0, nil +} + +// ExtractRoles returns a slice of Roles contained in a single page of results. +func ExtractRoles(page pagination.Page) ([]Role, error) { + casted := page.(RolePage).Body + var response struct { + Roles []Role `mapstructure:"roles"` + } + + err := mapstructure.Decode(casted, &response) + return response.Roles, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as a User, if possible. +func (r commonResult) Extract() (*User, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + User User `mapstructure:"user"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.User, err +} + +// CreateResult represents the result of a Create operation +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation +type DeleteResult struct { + commonResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/urls.go new file mode 100644 index 00000000000..7ec4385d743 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/urls.go @@ -0,0 +1,21 @@ +package users + +import "github.com/rackspace/gophercloud" + +const ( + tenantPath = "tenants" + userPath = "users" + rolePath = "roles" +) + +func ResourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(userPath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(userPath) +} + +func listRolesURL(c *gophercloud.ServiceClient, tenantID, userID string) string { + return c.ServiceURL(tenantPath, tenantID, userPath, userID, rolePath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go index 32e6d1ba7d7..42f05d365a5 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go @@ -38,7 +38,7 @@ func TestCreateSuccessful(t *testing.T) { } if result.Description == nil || *result.Description != "Here's your service" { - t.Errorf("Service description was unexpected [%s]", result.Description) + t.Errorf("Service description was unexpected [%s]", *result.Description) } if result.ID != "1234" { t.Errorf("Service ID was unexpected [%s]", result.ID) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go index 58ec580dbce..25865841a48 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go @@ -63,7 +63,7 @@ type CreateOpts struct { // load balancer pool member. func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { type member struct { - TenantID string `json:"tenant_id"` + TenantID string `json:"tenant_id,omitempty"` ProtocolPort int `json:"protocol_port"` Address string `json:"address"` PoolID string `json:"pool_id"` diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go index eaa713600ea..dedbb252de9 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go @@ -188,7 +188,7 @@ func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuild } // Send request to API - _, res.Err = perigee.Request("PUT", getURL(c, networkID), perigee.Options{ + _, res.Err = perigee.Request("PUT", updateURL(c, networkID), perigee.Options{ MoreHeaders: c.AuthenticatedHeaders(), ReqBody: &reqBody, Results: &res.Body, diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls.go index 33c23875caa..a9eecc52956 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls.go @@ -22,6 +22,10 @@ func createURL(c *gophercloud.ServiceClient) string { return rootURL(c) } +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + func deleteURL(c *gophercloud.ServiceClient, id string) string { return resourceURL(c, id) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go index 3399907dd3e..06d273ef19e 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go @@ -164,7 +164,6 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { ReqBody: &reqBody, Results: &res.Body, OkCodes: []int{201}, - DumpReqJson: true, }) return res diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go index 3dad0c5a9be..f22b6870053 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go @@ -14,9 +14,8 @@ import ( // responds with a `Get` response. func HandleGetAccountSuccessfully(t *testing.T) { th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - th.TestMethod(t, r, "POST") + th.TestMethod(t, r, "HEAD") th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) - th.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts") w.Header().Set("X-Account-Container-Count", "2") w.Header().Set("X-Account-Bytes-Used", "14") @@ -30,9 +29,10 @@ func HandleGetAccountSuccessfully(t *testing.T) { // responds with a `Update` response. func HandleUpdateAccountSuccessfully(t *testing.T) { th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - th.TestMethod(t, r, "HEAD") + th.TestMethod(t, r, "POST") th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) - w.Header().Set("X-Account-Meta-Foo", "bar") + th.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts") + w.WriteHeader(http.StatusNoContent) }) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go index d6dc26b650e..6454c0ac4ca 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go @@ -7,12 +7,10 @@ import ( fake "github.com/rackspace/gophercloud/testhelper/client" ) -var metadata = map[string]string{"gophercloud-test": "accounts"} - func TestUpdateAccount(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - HandleGetAccountSuccessfully(t) + HandleUpdateAccountSuccessfully(t) options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}} res := Update(fake.ServiceClient(), options) @@ -22,12 +20,13 @@ func TestUpdateAccount(t *testing.T) { func TestGetAccount(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - HandleUpdateAccountSuccessfully(t) + HandleGetAccountSuccessfully(t) - expected := map[string]string{"Foo": "bar"} - actual, err := Get(fake.ServiceClient(), &GetOpts{}).ExtractMetadata() - if err != nil { - t.Fatalf("Unable to get account metadata: %v", err) - } - th.CheckDeepEquals(t, expected, actual) + expectedMetadata := map[string]string{"Subject": "books"} + res := Get(fake.ServiceClient(), &GetOpts{}) + th.AssertNoErr(t, res.Err) + actualMetadata, _ := res.ExtractMetadata() + th.CheckDeepEquals(t, expectedMetadata, actualMetadata) + //headers, err := res.Extract() + //th.AssertNoErr(t, err) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go index abae02659cf..6ab1a230617 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go @@ -2,15 +2,88 @@ package accounts import ( "strings" + "time" "github.com/rackspace/gophercloud" ) +// UpdateResult is returned from a call to the Update function. +type UpdateResult struct { + gophercloud.HeaderResult +} + +// UpdateHeader represents the headers returned in the response from an Update request. +type UpdateHeader struct { + ContentLength string `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + TransID string `mapstructure:"X-Trans-Id"` +} + +// Extract will return a struct of headers returned from a call to Get. To obtain +// a map of headers, call the ExtractHeader method on the GetResult. +func (ur UpdateResult) Extract() (UpdateHeader, error) { + var uh UpdateHeader + if ur.Err != nil { + return uh, ur.Err + } + + if err := gophercloud.DecodeHeader(ur.Header, &uh); err != nil { + return uh, err + } + + if date, ok := ur.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, ur.Header["Date"][0]) + if err != nil { + return uh, err + } + uh.Date = t + } + + return uh, nil +} + +// GetHeader represents the headers returned in the response from a Get request. +type GetHeader struct { + BytesUsed int64 `mapstructure:"X-Account-Bytes-Used"` + ContainerCount int `mapstructure:"X-Account-Container-Count"` + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + ObjectCount int64 `mapstructure:"X-Account-Object-Count"` + TransID string `mapstructure:"X-Trans-Id"` + TempURLKey string `mapstructure:"X-Account-Meta-Temp-URL-Key"` + TempURLKey2 string `mapstructure:"X-Account-Meta-Temp-URL-Key-2"` +} + // GetResult is returned from a call to the Get function. type GetResult struct { gophercloud.HeaderResult } +// Extract will return a struct of headers returned from a call to Get. To obtain +// a map of headers, call the ExtractHeader method on the GetResult. +func (gr GetResult) Extract() (GetHeader, error) { + var gh GetHeader + if gr.Err != nil { + return gh, gr.Err + } + + if err := gophercloud.DecodeHeader(gr.Header, &gh); err != nil { + return gh, err + } + + if date, ok := gr.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, gr.Header["Date"][0]) + if err != nil { + return gh, err + } + gh.Date = t + } + + return gh, nil +} + // ExtractMetadata is a function that takes a GetResult (of type *http.Response) // and returns the custom metatdata associated with the account. func (gr GetResult) ExtractMetadata() (map[string]string, error) { @@ -27,8 +100,3 @@ func (gr GetResult) ExtractMetadata() (map[string]string, error) { } return metadata, nil } - -// UpdateResult is returned from a call to the Update function. -type UpdateResult struct { - gophercloud.HeaderResult -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go index 1c0a915cb7d..9c84bce0a4c 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go @@ -94,6 +94,7 @@ func HandleCreateContainerSuccessfully(t *testing.T) { th.TestHeader(t, r, "Accept", "application/json") w.Header().Add("X-Container-Meta-Foo", "bar") + w.Header().Add("X-Trans-Id", "1234567") w.WriteHeader(http.StatusNoContent) }) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go index d0ce7f1e585..f650696e485 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go @@ -58,8 +58,10 @@ func TestCreateContainer(t *testing.T) { options := CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}} res := Create(fake.ServiceClient(), "testContainer", options) - th.CheckNoErr(t, res.Err) + c, err := res.Extract() + th.CheckNoErr(t, err) th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0]) + th.CheckEquals(t, "1234567", c.TransID) } func TestDeleteContainer(t *testing.T) { diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go index 74f32860465..e682b8dccbf 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go @@ -3,6 +3,7 @@ package containers import ( "fmt" "strings" + "time" "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" @@ -96,11 +97,48 @@ func ExtractNames(page pagination.Page) ([]string, error) { } } +// GetHeader represents the headers returned in the response from a Get request. +type GetHeader struct { + AcceptRanges string `mapstructure:"Accept-Ranges"` + BytesUsed int64 `mapstructure:"X-Account-Bytes-Used"` + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + ObjectCount int64 `mapstructure:"X-Container-Object-Count"` + Read string `mapstructure:"X-Container-Read"` + TransID string `mapstructure:"X-Trans-Id"` + VersionsLocation string `mapstructure:"X-Versions-Location"` + Write string `mapstructure:"X-Container-Write"` +} + // GetResult represents the result of a get operation. type GetResult struct { gophercloud.HeaderResult } +// Extract will return a struct of headers returned from a call to Get. To obtain +// a map of headers, call the ExtractHeader method on the GetResult. +func (gr GetResult) Extract() (GetHeader, error) { + var gh GetHeader + if gr.Err != nil { + return gh, gr.Err + } + + if err := gophercloud.DecodeHeader(gr.Header, &gh); err != nil { + return gh, err + } + + if date, ok := gr.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, gr.Header["Date"][0]) + if err != nil { + return gh, err + } + gh.Date = t + } + + return gh, nil +} + // ExtractMetadata is a function that takes a GetResult (of type *http.Response) // and returns the custom metadata associated with the container. func (gr GetResult) ExtractMetadata() (map[string]string, error) { @@ -117,6 +155,14 @@ func (gr GetResult) ExtractMetadata() (map[string]string, error) { return metadata, nil } +// CreateHeader represents the headers returned in the response from a Create request. +type CreateHeader struct { + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + TransID string `mapstructure:"X-Trans-Id"` +} + // CreateResult represents the result of a create operation. To extract the // the headers from the HTTP response, you can invoke the 'ExtractHeader' // method on the result struct. @@ -124,6 +170,37 @@ type CreateResult struct { gophercloud.HeaderResult } +// Extract will return a struct of headers returned from a call to Create. To obtain +// a map of headers, call the ExtractHeader method on the CreateResult. +func (cr CreateResult) Extract() (CreateHeader, error) { + var ch CreateHeader + if cr.Err != nil { + return ch, cr.Err + } + + if err := gophercloud.DecodeHeader(cr.Header, &ch); err != nil { + return ch, err + } + + if date, ok := cr.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, cr.Header["Date"][0]) + if err != nil { + return ch, err + } + ch.Date = t + } + + return ch, nil +} + +// UpdateHeader represents the headers returned in the response from a Update request. +type UpdateHeader struct { + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + TransID string `mapstructure:"X-Trans-Id"` +} + // UpdateResult represents the result of an update operation. To extract the // the headers from the HTTP response, you can invoke the 'ExtractHeader' // method on the result struct. @@ -131,9 +208,63 @@ type UpdateResult struct { gophercloud.HeaderResult } +// Extract will return a struct of headers returned from a call to Update. To obtain +// a map of headers, call the ExtractHeader method on the UpdateResult. +func (ur UpdateResult) Extract() (UpdateHeader, error) { + var uh UpdateHeader + if ur.Err != nil { + return uh, ur.Err + } + + if err := gophercloud.DecodeHeader(ur.Header, &uh); err != nil { + return uh, err + } + + if date, ok := ur.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, ur.Header["Date"][0]) + if err != nil { + return uh, err + } + uh.Date = t + } + + return uh, nil +} + +// DeleteHeader represents the headers returned in the response from a Delete request. +type DeleteHeader struct { + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + TransID string `mapstructure:"X-Trans-Id"` +} + // DeleteResult represents the result of a delete operation. To extract the // the headers from the HTTP response, you can invoke the 'ExtractHeader' // method on the result struct. type DeleteResult struct { gophercloud.HeaderResult } + +// Extract will return a struct of headers returned from a call to Delete. To obtain +// a map of headers, call the ExtractHeader method on the DeleteResult. +func (dr DeleteResult) Extract() (DeleteHeader, error) { + var dh DeleteHeader + if dr.Err != nil { + return dh, dr.Err + } + + if err := gophercloud.DecodeHeader(dr.Header, &dh); err != nil { + return dh, err + } + + if date, ok := dr.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, dr.Header["Date"][0]) + if err != nil { + return dh, err + } + dh.Date = t + } + + return dh, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go index d951160e3a1..ec616371a3c 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go @@ -105,13 +105,31 @@ func HandleListObjectNamesSuccessfully(t *testing.T) { }) } -// HandleCreateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that -// responds with a `Create` response. -func HandleCreateObjectSuccessfully(t *testing.T) { +// HandleCreateTextObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux +// that responds with a `Create` response. A Content-Type of "text/plain" is expected. +func HandleCreateTextObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", 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, "Content-Type", "text/plain") + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusCreated) + }) +} + +// HandleCreateTypelessObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler +// mux that responds with a `Create` response. No Content-Type header may be present in the request, so that server- +// side content-type detection will be triggered properly. +func HandleCreateTypelessObjectSuccessfully(t *testing.T) { th.Mux.HandleFunc("/testContainer/testObject", 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") + + if contentType, present := r.Header["Content-Type"]; present { + t.Errorf("Expected Content-Type header to be omitted, but was %#v", contentType) + } + w.WriteHeader(http.StatusCreated) }) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go index 9778de3be6d..def1798a5b4 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go @@ -1,12 +1,16 @@ package objects import ( + "crypto/hmac" + "crypto/sha1" "fmt" "io" + "strings" "time" "github.com/racker/perigee" "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" "github.com/rackspace/gophercloud/pagination" ) @@ -18,6 +22,10 @@ type ListOptsBuilder interface { // ListOpts is a structure that holds parameters for listing objects. type ListOpts struct { + // Full is a true/false value that represents the amount of object information + // returned. If Full is set to true, then the content-type, number of bytes, hash + // date last modified, and name are returned. If set to false or not set, then + // only the object names are returned. Full bool Limit int `q:"limit"` Marker string `q:"marker"` @@ -88,7 +96,7 @@ type DownloadOpts struct { // ToObjectDownloadParams formats a DownloadOpts into a query string and map of // headers. -func (opts ListOpts) ToObjectDownloadParams() (map[string]string, string, error) { +func (opts DownloadOpts) ToObjectDownloadParams() (map[string]string, string, error) { q, err := gophercloud.BuildQueryString(opts) if err != nil { return nil, "", err @@ -125,7 +133,7 @@ func Download(c *gophercloud.ServiceClient, containerName, objectName string, op resp, err := perigee.Request("GET", url, perigee.Options{ MoreHeaders: h, - OkCodes: []int{200}, + OkCodes: []int{200, 304}, }) res.Body = resp.HttpResponse.Body @@ -146,7 +154,7 @@ type CreateOpts struct { Metadata map[string]string ContentDisposition string `h:"Content-Disposition"` ContentEncoding string `h:"Content-Encoding"` - ContentLength int `h:"Content-Length"` + ContentLength int64 `h:"Content-Length"` ContentType string `h:"Content-Type"` CopyFrom string `h:"X-Copy-From"` DeleteAfter int `h:"X-Delete-After"` @@ -201,14 +209,20 @@ func Create(c *gophercloud.ServiceClient, containerName, objectName string, cont url += query } - contentType := h["Content-Type"] - - resp, err := perigee.Request("PUT", url, perigee.Options{ - ContentType: contentType, + popts := perigee.Options{ ReqBody: content, MoreHeaders: h, OkCodes: []int{201, 202}, - }) + } + + if contentType, explicit := h["Content-Type"]; explicit { + popts.ContentType = contentType + delete(h, "Content-Type") + } else { + popts.OmitContentType = true + } + + resp, err := perigee.Request("PUT", url, popts) res.Header = resp.HttpResponse.Header res.Err = err return res @@ -414,3 +428,51 @@ func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts res.Err = err return res } + +// HTTPMethod represents an HTTP method string (e.g. "GET"). +type HTTPMethod string + +var ( + // GET represents an HTTP "GET" method. + GET HTTPMethod = "GET" + // POST represents an HTTP "POST" method. + POST HTTPMethod = "POST" +) + +// CreateTempURLOpts are options for creating a temporary URL for an object. +type CreateTempURLOpts struct { + // (REQUIRED) Method is the HTTP method to allow for users of the temp URL. Valid values + // are "GET" and "POST". + Method HTTPMethod + // (REQUIRED) TTL is the number of seconds the temp URL should be active. + TTL int + // (Optional) Split is the string on which to split the object URL. Since only + // the object path is used in the hash, the object URL needs to be parsed. If + // empty, the default OpenStack URL split point will be used ("/v1/"). + Split string +} + +// CreateTempURL is a function for creating a temporary URL for an object. It +// allows users to have "GET" or "POST" access to a particular tenant's object +// for a limited amount of time. +func CreateTempURL(c *gophercloud.ServiceClient, containerName, objectName string, opts CreateTempURLOpts) (string, error) { + if opts.Split == "" { + opts.Split = "/v1/" + } + duration := time.Duration(opts.TTL) * time.Second + expiry := time.Now().Add(duration).Unix() + getHeader, err := accounts.Get(c, nil).Extract() + if err != nil { + return "", err + } + secretKey := []byte(getHeader.TempURLKey) + url := getURL(c, containerName, objectName) + splitPath := strings.Split(url, opts.Split) + baseURL, objectPath := splitPath[0], splitPath[1] + objectPath = opts.Split + objectPath + body := fmt.Sprintf("%s\n%d\n%s", opts.Method, expiry, objectPath) + hash := hmac.New(sha1.New, secretKey) + hash.Write([]byte(body)) + hexsum := fmt.Sprintf("%x", hash.Sum(nil)) + return fmt.Sprintf("%s%s?temp_url_sig=%s&temp_url_expires=%d", baseURL, objectPath, hexsum, expiry), nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go index c3c28a789b9..6be3534205f 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go @@ -83,14 +83,24 @@ func TestListObjectNames(t *testing.T) { func TestCreateObject(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - HandleCreateObjectSuccessfully(t) + HandleCreateTextObjectSuccessfully(t) content := bytes.NewBufferString("Did gyre and gimble in the wabe") - options := &CreateOpts{ContentType: "application/json"} + options := &CreateOpts{ContentType: "text/plain"} res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options) th.AssertNoErr(t, res.Err) } +func TestCreateObjectWithoutContentType(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateTypelessObjectSuccessfully(t) + + content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.") + res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &CreateOpts{}) + th.AssertNoErr(t, res.Err) +} + func TestCopyObject(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go index 102d94cb37c..ecb2c54582f 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go @@ -4,7 +4,9 @@ import ( "fmt" "io" "io/ioutil" + "strconv" "strings" + "time" "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" @@ -14,11 +16,22 @@ import ( // Object is a structure that holds information related to a storage object. type Object struct { - Bytes int `json:"bytes" mapstructure:"bytes"` - ContentType string `json:"content_type" mapstructure:"content_type"` - Hash string `json:"hash" mapstructure:"hash"` + // Bytes is the total number of bytes that comprise the object. + Bytes int64 `json:"bytes" mapstructure:"bytes"` + + // ContentType is the content type of the object. + ContentType string `json:"content_type" mapstructure:"content_type"` + + // Hash represents the MD5 checksum value of the object's content. + Hash string `json:"hash" mapstructure:"hash"` + + // LastModified is the RFC3339Milli time the object was last modified, represented + // as a string. For any given object (obj), this value may be parsed to a time.Time: + // lastModified, err := time.Parse(gophercloud.RFC3339Milli, obj.LastModified) LastModified string `json:"last_modified" mapstructure:"last_modified"` - Name string `json:"name" mapstructure:"name"` + + // Name is the unique name for the object. + Name string `json:"name" mapstructure:"name"` } // ObjectPage is a single page of objects that is returned from a call to the @@ -97,12 +110,67 @@ func ExtractNames(page pagination.Page) ([]string, error) { } } +// DownloadHeader represents the headers returned in the response from a Download request. +type DownloadHeader struct { + AcceptRanges string `mapstructure:"Accept-Ranges"` + ContentDisposition string `mapstructure:"Content-Disposition"` + ContentEncoding string `mapstructure:"Content-Encoding"` + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + DeleteAt time.Time `mapstructure:"-"` + ETag string `mapstructure:"Etag"` + LastModified time.Time `mapstructure:"-"` + ObjectManifest string `mapstructure:"X-Object-Manifest"` + StaticLargeObject bool `mapstructure:"X-Static-Large-Object"` + TransID string `mapstructure:"X-Trans-Id"` +} + // DownloadResult is a *http.Response that is returned from a call to the Download function. type DownloadResult struct { gophercloud.HeaderResult Body io.ReadCloser } +// Extract will return a struct of headers returned from a call to Download. To obtain +// a map of headers, call the ExtractHeader method on the DownloadResult. +func (dr DownloadResult) Extract() (DownloadHeader, error) { + var dh DownloadHeader + if dr.Err != nil { + return dh, dr.Err + } + + if err := gophercloud.DecodeHeader(dr.Header, &dh); err != nil { + return dh, err + } + + if date, ok := dr.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, date[0]) + if err != nil { + return dh, err + } + dh.Date = t + } + + if date, ok := dr.Header["Last-Modified"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, date[0]) + if err != nil { + return dh, err + } + dh.LastModified = t + } + + if date, ok := dr.Header["X-Delete-At"]; ok && len(date) > 0 { + unix, err := strconv.ParseInt(date[0], 10, 64) + if err != nil { + return dh, err + } + dh.DeleteAt = time.Unix(unix, 0) + } + + return dh, nil +} + // ExtractContent is a function that takes a DownloadResult's io.Reader body // and reads all available data into a slice of bytes. Please be aware that due // the nature of io.Reader is forward-only - meaning that it can only be read @@ -120,11 +188,65 @@ func (dr DownloadResult) ExtractContent() ([]byte, error) { return body, nil } +// GetHeader represents the headers returned in the response from a Get request. +type GetHeader struct { + ContentDisposition string `mapstructure:"Content-Disposition"` + ContentEncoding string `mapstructure:"Content-Encoding"` + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + DeleteAt time.Time `mapstructure:"-"` + ETag string `mapstructure:"Etag"` + LastModified time.Time `mapstructure:"-"` + ObjectManifest string `mapstructure:"X-Object-Manifest"` + StaticLargeObject bool `mapstructure:"X-Static-Large-Object"` + TransID string `mapstructure:"X-Trans-Id"` +} + // GetResult is a *http.Response that is returned from a call to the Get function. type GetResult struct { gophercloud.HeaderResult } +// Extract will return a struct of headers returned from a call to Get. To obtain +// a map of headers, call the ExtractHeader method on the GetResult. +func (gr GetResult) Extract() (GetHeader, error) { + var gh GetHeader + if gr.Err != nil { + return gh, gr.Err + } + + if err := gophercloud.DecodeHeader(gr.Header, &gh); err != nil { + return gh, err + } + + if date, ok := gr.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, gr.Header["Date"][0]) + if err != nil { + return gh, err + } + gh.Date = t + } + + if date, ok := gr.Header["Last-Modified"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, gr.Header["Last-Modified"][0]) + if err != nil { + return gh, err + } + gh.LastModified = t + } + + if date, ok := gr.Header["X-Delete-At"]; ok && len(date) > 0 { + unix, err := strconv.ParseInt(date[0], 10, 64) + if err != nil { + return gh, err + } + gh.DeleteAt = time.Unix(unix, 0) + } + + return gh, nil +} + // ExtractMetadata is a function that takes a GetResult (of type *http.Response) // and returns the custom metadata associated with the object. func (gr GetResult) ExtractMetadata() (map[string]string, error) { @@ -141,22 +263,176 @@ func (gr GetResult) ExtractMetadata() (map[string]string, error) { return metadata, nil } +// CreateHeader represents the headers returned in the response from a Create request. +type CreateHeader struct { + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + ETag string `mapstructure:"Etag"` + LastModified time.Time `mapstructure:"-"` + TransID string `mapstructure:"X-Trans-Id"` +} + // CreateResult represents the result of a create operation. type CreateResult struct { gophercloud.HeaderResult } +// Extract will return a struct of headers returned from a call to Create. To obtain +// a map of headers, call the ExtractHeader method on the CreateResult. +func (cr CreateResult) Extract() (CreateHeader, error) { + var ch CreateHeader + if cr.Err != nil { + return ch, cr.Err + } + + if err := gophercloud.DecodeHeader(cr.Header, &ch); err != nil { + return ch, err + } + + if date, ok := cr.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, cr.Header["Date"][0]) + if err != nil { + return ch, err + } + ch.Date = t + } + + if date, ok := cr.Header["Last-Modified"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, cr.Header["Last-Modified"][0]) + if err != nil { + return ch, err + } + ch.LastModified = t + } + + return ch, nil +} + +// UpdateHeader represents the headers returned in the response from a Update request. +type UpdateHeader struct { + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + TransID string `mapstructure:"X-Trans-Id"` +} + // UpdateResult represents the result of an update operation. type UpdateResult struct { gophercloud.HeaderResult } +// Extract will return a struct of headers returned from a call to Update. To obtain +// a map of headers, call the ExtractHeader method on the UpdateResult. +func (ur UpdateResult) Extract() (UpdateHeader, error) { + var uh UpdateHeader + if ur.Err != nil { + return uh, ur.Err + } + + if err := gophercloud.DecodeHeader(ur.Header, &uh); err != nil { + return uh, err + } + + if date, ok := ur.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, ur.Header["Date"][0]) + if err != nil { + return uh, err + } + uh.Date = t + } + + return uh, nil +} + +// DeleteHeader represents the headers returned in the response from a Delete request. +type DeleteHeader struct { + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + TransID string `mapstructure:"X-Trans-Id"` +} + // DeleteResult represents the result of a delete operation. type DeleteResult struct { gophercloud.HeaderResult } +// Extract will return a struct of headers returned from a call to Delete. To obtain +// a map of headers, call the ExtractHeader method on the DeleteResult. +func (dr DeleteResult) Extract() (DeleteHeader, error) { + var dh DeleteHeader + if dr.Err != nil { + return dh, dr.Err + } + + if err := gophercloud.DecodeHeader(dr.Header, &dh); err != nil { + return dh, err + } + + if date, ok := dr.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, dr.Header["Date"][0]) + if err != nil { + return dh, err + } + dh.Date = t + } + + return dh, nil +} + +// CopyHeader represents the headers returned in the response from a Copy request. +type CopyHeader struct { + ContentLength int64 `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + CopiedFrom string `mapstructure:"X-Copied-From"` + CopiedFromLastModified time.Time `mapstructure:"-"` + Date time.Time `mapstructure:"-"` + ETag string `mapstructure:"Etag"` + LastModified time.Time `mapstructure:"-"` + TransID string `mapstructure:"X-Trans-Id"` +} + // CopyResult represents the result of a copy operation. type CopyResult struct { gophercloud.HeaderResult } + +// Extract will return a struct of headers returned from a call to Copy. To obtain +// a map of headers, call the ExtractHeader method on the CopyResult. +func (cr CopyResult) Extract() (CopyHeader, error) { + var ch CopyHeader + if cr.Err != nil { + return ch, cr.Err + } + + if err := gophercloud.DecodeHeader(cr.Header, &ch); err != nil { + return ch, err + } + + if date, ok := cr.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, cr.Header["Date"][0]) + if err != nil { + return ch, err + } + ch.Date = t + } + + if date, ok := cr.Header["Last-Modified"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, cr.Header["Last-Modified"][0]) + if err != nil { + return ch, err + } + ch.LastModified = t + } + + if date, ok := cr.Header["X-Copied-From-Last-Modified"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, cr.Header["X-Copied-From-Last-Modified"][0]) + if err != nil { + return ch, err + } + ch.CopiedFromLastModified = t + } + + return ch, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go deleted file mode 100644 index e8c2e82a050..00000000000 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Package gophercloud provides a multi-vendor interface to OpenStack-compatible -clouds. The library has a three-level hierarchy: providers, services, and -resources. - -Provider structs represent the service providers that offer and manage a -collection of services. Examples of providers include: OpenStack, Rackspace, -HP. These are defined like so: - - opts := gophercloud.AuthOptions{ - IdentityEndpoint: "https://my-openstack.com:5000/v2.0", - Username: "{username}", - Password: "{password}", - TenantID: "{tenant_id}", - } - - provider, err := openstack.AuthenticatedClient(opts) - -Service structs are specific to a provider and handle all of the logic and -operations for a particular OpenStack service. Examples of services include: -Compute, Object Storage, Block Storage. In order to define one, you need to -pass in the parent provider, like so: - - opts := gophercloud.EndpointOpts{Region: "RegionOne"} - - client := openstack.NewComputeV2(provider, opts) - -Resource structs are the domain models that services make use of in order -to work with and represent the state of API resources: - - server, err := servers.Get(client, "{serverId}").Extract() - -Another convention is to return Result structs for API operations, which allow -you to access the HTTP headers, response body, and associated errors with the -network transaction. To get a resource struct, you then call the Extract -method which is chained to the response. -*/ -package gophercloud diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go index 5fe3c2cceaa..948783b0738 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go @@ -9,11 +9,35 @@ import ( "time" ) -// MaybeString takes a string that might be a zero-value, and either returns a -// pointer to its address or a nil value (i.e. empty pointer). This is useful -// for converting zero values in options structs when the end-user hasn't -// defined values. Those zero values need to be nil in order for the JSON -// serialization to ignore them. +// EnabledState is a convenience type, mostly used in Create and Update +// operations. Because the zero value of a bool is FALSE, we need to use a +// pointer instead to indicate zero-ness. +type EnabledState *bool + +// Convenience vars for EnabledState values. +var ( + iTrue = true + iFalse = false + + Enabled EnabledState = &iTrue + Disabled EnabledState = &iFalse +) + +// IntToPointer is a function for converting integers into integer pointers. +// This is useful when passing in options to operations. +func IntToPointer(i int) *int { + return &i +} + +/* +MaybeString is an internal function to be used by request methods in individual +resource packages. + +It takes a string that might be a zero value and returns either a pointer to its +address or nil. This is useful for allowing users to conveniently omit values +from an options struct by leaving them zeroed, but still pass nil to the JSON +serializer so they'll be omitted from the request body. +*/ func MaybeString(original string) *string { if original != "" { return &original @@ -21,8 +45,14 @@ func MaybeString(original string) *string { return nil } -// MaybeInt takes an int that might be a zero-value, and either returns a -// pointer to its address or a nil value (i.e. empty pointer). +/* +MaybeInt is an internal function to be used by request methods in individual +resource packages. + +Like MaybeString, it accepts an int that may or may not be a zero value, and +returns either a pointer to its address or nil. It's intended to hint that the +JSON serializer should omit its field. +*/ func MaybeInt(original int) *int { if original != 0 { return &original @@ -61,19 +91,26 @@ func isZero(v reflect.Value) bool { } /* -BuildQueryString accepts a generic structure and parses it URL struct. It -converts field names into query names based on "q" tags. So for example, this -type: +BuildQueryString is an internal function to be used by request methods in +individual resource packages. - struct { +It accepts a tagged structure and expands it into a URL struct. Field names are +converted into query parameters based on a "q" tag. For example: + + type struct Something { Bar string `q:"x_bar"` Baz int `q:"lorem_ipsum"` - }{ - Bar: "XXX", - Baz: "YYY", } -will be converted into ?x_bar=XXX&lorem_ipsum=YYYY + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into "?x_bar=AAA&lorem_ipsum=BBB". + +The struct's fields may be strings, integers, or boolean values. Fields left at +their type's zero value will be omitted from the query. */ func BuildQueryString(opts interface{}) (*url.URL, error) { optsValue := reflect.ValueOf(opts) @@ -86,7 +123,8 @@ func BuildQueryString(opts interface{}) (*url.URL, error) { optsType = optsType.Elem() } - var optsSlice []string + params := url.Values{} + if optsValue.Kind() == reflect.Struct { for i := 0; i < optsValue.NumField(); i++ { v := optsValue.Field(i) @@ -101,11 +139,11 @@ func BuildQueryString(opts interface{}) (*url.URL, error) { if !isZero(v) { switch v.Kind() { case reflect.String: - optsSlice = append(optsSlice, tags[0]+"="+v.String()) + params.Add(tags[0], v.String()) case reflect.Int: - optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatInt(v.Int(), 10)) + params.Add(tags[0], strconv.FormatInt(v.Int(), 10)) case reflect.Bool: - optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatBool(v.Bool())) + params.Add(tags[0], strconv.FormatBool(v.Bool())) } } else { // Otherwise, the field is not set. @@ -115,26 +153,42 @@ func BuildQueryString(opts interface{}) (*url.URL, error) { } } } + } - } - // URL encode the string for safety. - s := strings.Join(optsSlice, "&") - if s != "" { - s = "?" + s - } - u, err := url.Parse(s) - if err != nil { - return nil, err - } - return u, nil + return &url.URL{RawQuery: params.Encode()}, nil } // Return an error if the underlying type of 'opts' isn't a struct. return nil, fmt.Errorf("Options type is not a struct.") } -// BuildHeaders accepts a generic structure and parses it into a string map. It -// converts field names into header names based on "h" tags, and field values -// into header values by a simple one-to-one mapping. +/* +BuildHeaders is an internal function to be used by request methods in +individual resource packages. + +It accepts an arbitrary tagged structure and produces a string map that's +suitable for use as the HTTP headers of an outgoing request. Field names are +mapped to header names based in "h" tags. + + type struct Something { + Bar string `h:"x_bar"` + Baz int `h:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into: + + map[string]string{ + "x_bar": "AAA", + "lorem_ipsum": "BBB", + } + +Untagged fields and fields left at their zero values are skipped. Integers, +booleans and string values are supported. +*/ func BuildHeaders(opts interface{}) (map[string]string, error) { optsValue := reflect.ValueOf(opts) if optsValue.Kind() == reflect.Ptr { @@ -182,3 +236,25 @@ func BuildHeaders(opts interface{}) (map[string]string, error) { // Return an error if the underlying type of 'opts' isn't a struct. return optsMap, fmt.Errorf("Options type is not a struct.") } + +// IDSliceToQueryString takes a slice of elements and converts them into a query +// string. For example, if name=foo and slice=[]int{20, 40, 60}, then the +// result would be `?name=20&name=40&name=60' +func IDSliceToQueryString(name string, ids []int) string { + str := "" + for k, v := range ids { + if k == 0 { + str += "?" + } else { + str += "&" + } + str += fmt.Sprintf("%s=%s", name, strconv.Itoa(v)) + } + return str +} + +// IntWithinRange returns TRUE if an integer falls within a defined range, and +// FALSE if not. +func IntWithinRange(val, min, max int) bool { + return val > min && val < max +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/params_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params_test.go index 9f1d3bdcb01..4a2c9fe3413 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/params_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params_test.go @@ -43,7 +43,7 @@ func TestBuildQueryString(t *testing.T) { R: "red", C: true, } - expected := &url.URL{RawQuery: "j=2&r=red&c=true"} + expected := &url.URL{RawQuery: "c=true&j=2&r=red"} actual, err := BuildQueryString(&opts) if err != nil { t.Errorf("Error building query string: %v", err) @@ -138,5 +138,18 @@ func TestIsZero(t *testing.T) { expected = false actual = isZero(testStructValue) th.CheckEquals(t, expected, actual) - +} + +func TestQueriesAreEscaped(t *testing.T) { + type foo struct { + Name string `q:"something"` + Shape string `q:"else"` + } + + expected := &url.URL{RawQuery: "else=Triangl+e&something=blah%2B%3F%21%21foo"} + + actual, err := BuildQueryString(foo{Name: "blah+?!!foo", Shape: "Triangl e"}) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expected, actual) } diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/delegate.go new file mode 100644 index 00000000000..5af7e077844 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/delegate.go @@ -0,0 +1,18 @@ +package base + +import ( + "github.com/rackspace/gophercloud" + + os "github.com/rackspace/gophercloud/openstack/cdn/v1/base" +) + +// Get retrieves the home document, allowing the user to discover the +// entire API. +func Get(c *gophercloud.ServiceClient) os.GetResult { + return os.Get(c) +} + +// Ping retrieves a ping to the server. +func Ping(c *gophercloud.ServiceClient) os.PingResult { + return os.Ping(c) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/delegate_test.go new file mode 100644 index 00000000000..3c058016a06 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/delegate_test.go @@ -0,0 +1,44 @@ +package base + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/cdn/v1/base" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestGetHomeDocument(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetSuccessfully(t) + + actual, err := Get(fake.ServiceClient()).Extract() + th.CheckNoErr(t, err) + + expected := os.HomeDocument{ + "rel/cdn": map[string]interface{}{ + "href-template": "services{?marker,limit}", + "href-vars": map[string]interface{}{ + "marker": "param/marker", + "limit": "param/limit", + }, + "hints": map[string]interface{}{ + "allow": []string{"GET"}, + "formats": map[string]interface{}{ + "application/json": map[string]interface{}{}, + }, + }, + }, + } + th.CheckDeepEquals(t, expected, *actual) +} + +func TestPing(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandlePingSuccessfully(t) + + err := Ping(fake.ServiceClient()).ExtractErr() + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/doc.go new file mode 100644 index 00000000000..5582306a8e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/base/doc.go @@ -0,0 +1,4 @@ +// Package base provides information and interaction with the base API +// resource in the Rackspace CDN service. This API resource allows for +// retrieving the Home Document and pinging the root URL. +package base diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/delegate.go new file mode 100644 index 00000000000..7152fa23af3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/delegate.go @@ -0,0 +1,18 @@ +package flavors + +import ( + "github.com/rackspace/gophercloud" + + os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a single page of CDN flavors. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return os.List(c) +} + +// Get retrieves a specific flavor based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/delegate_test.go new file mode 100644 index 00000000000..d6d299df8ed --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/delegate_test.go @@ -0,0 +1,90 @@ +package flavors + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.HandleListCDNFlavorsSuccessfully(t) + + count := 0 + + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractFlavors(page) + if err != nil { + t.Errorf("Failed to extract flavors: %v", err) + return false, err + } + + expected := []os.Flavor{ + os.Flavor{ + ID: "europe", + Providers: []os.Provider{ + os.Provider{ + Provider: "Fastly", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://www.fastly.com", + Rel: "provider_url", + }, + }, + }, + }, + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/europe", + Rel: "self", + }, + }, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.HandleGetCDNFlavorSuccessfully(t) + + expected := &os.Flavor{ + ID: "asia", + Providers: []os.Provider{ + os.Provider{ + Provider: "ChinaCache", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://www.chinacache.com", + Rel: "provider_url", + }, + }, + }, + }, + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/asia", + Rel: "self", + }, + }, + } + + actual, err := Get(fake.ServiceClient(), "asia").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/doc.go new file mode 100644 index 00000000000..4ad966eac8d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors/doc.go @@ -0,0 +1,6 @@ +// Package flavors provides information and interaction with the flavors API +// resource in the Rackspace CDN service. This API resource allows for +// listing flavors and retrieving a specific flavor. +// +// A flavor is a mapping configuration to a CDN provider. +package flavors diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/delegate.go new file mode 100644 index 00000000000..07c93a8dcda --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/delegate.go @@ -0,0 +1,13 @@ +package serviceassets + +import ( + "github.com/rackspace/gophercloud" + + os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets" +) + +// Delete accepts a unique ID and deletes the CDN service asset associated with +// it. +func Delete(c *gophercloud.ServiceClient, id string, opts os.DeleteOptsBuilder) os.DeleteResult { + return os.Delete(c, id, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/delegate_test.go new file mode 100644 index 00000000000..328e1682d33 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/delegate_test.go @@ -0,0 +1,19 @@ +package serviceassets + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.HandleDeleteCDNAssetSuccessfully(t) + + err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/doc.go new file mode 100644 index 00000000000..46b3d50a81f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets/doc.go @@ -0,0 +1,7 @@ +// Package serviceassets provides information and interaction with the +// serviceassets API resource in the Rackspace CDN service. This API resource +// allows for deleting cached assets. +// +// A service distributes assets across the network. Service assets let you +// interrogate properties about these assets and perform certain actions on them. +package serviceassets diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/delegate.go new file mode 100644 index 00000000000..e3f14599773 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/delegate.go @@ -0,0 +1,37 @@ +package services + +import ( + "github.com/rackspace/gophercloud" + + os "github.com/rackspace/gophercloud/openstack/cdn/v1/services" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager which allows you to iterate over a collection of +// CDN services. It accepts a ListOpts struct, which allows for pagination via +// marker and limit. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// Create accepts a CreateOpts struct and creates a new CDN service using the +// values provided. +func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, opts) +} + +// Get retrieves a specific service based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(c, id) +} + +// Update accepts a UpdateOpts struct and updates an existing CDN service using +// the values provided. +func Update(c *gophercloud.ServiceClient, id string, patches []os.Patch) os.UpdateResult { + return os.Update(c, id, patches) +} + +// Delete accepts a unique ID and deletes the CDN service associated with it. +func Delete(c *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/delegate_test.go new file mode 100644 index 00000000000..6c48365e4ed --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/delegate_test.go @@ -0,0 +1,359 @@ +package services + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/cdn/v1/services" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.HandleListCDNServiceSuccessfully(t) + + count := 0 + + err := List(fake.ServiceClient(), &os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractServices(page) + if err != nil { + t.Errorf("Failed to extract services: %v", err) + return false, err + } + + expected := []os.Service{ + os.Service{ + ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Name: "mywebsite.com", + Domains: []os.Domain{ + os.Domain{ + Domain: "www.mywebsite.com", + }, + }, + Origins: []os.Origin{ + os.Origin{ + Origin: "mywebsite.com", + Port: 80, + SSL: false, + }, + }, + Caching: []os.CacheRule{ + os.CacheRule{ + Name: "default", + TTL: 3600, + }, + os.CacheRule{ + Name: "home", + TTL: 17200, + Rules: []os.TTLRule{ + os.TTLRule{ + Name: "index", + RequestURL: "/index.htm", + }, + }, + }, + os.CacheRule{ + Name: "images", + TTL: 12800, + Rules: []os.TTLRule{ + os.TTLRule{ + Name: "images", + RequestURL: "*.png", + }, + }, + }, + }, + Restrictions: []os.Restriction{ + os.Restriction{ + Name: "website only", + Rules: []os.RestrictionRule{ + os.RestrictionRule{ + Name: "mywebsite.com", + Referrer: "www.mywebsite.com", + }, + }, + }, + }, + FlavorID: "asia", + Status: "deployed", + Errors: []os.Error{}, + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Rel: "self", + }, + gophercloud.Link{ + Href: "mywebsite.com.cdn123.poppycdn.net", + Rel: "access_url", + }, + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/asia", + Rel: "flavor", + }, + }, + }, + os.Service{ + ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1", + Name: "myothersite.com", + Domains: []os.Domain{ + os.Domain{ + Domain: "www.myothersite.com", + }, + }, + Origins: []os.Origin{ + os.Origin{ + Origin: "44.33.22.11", + Port: 80, + SSL: false, + }, + os.Origin{ + Origin: "77.66.55.44", + Port: 80, + SSL: false, + Rules: []os.OriginRule{ + os.OriginRule{ + Name: "videos", + RequestURL: "^/videos/*.m3u", + }, + }, + }, + }, + Caching: []os.CacheRule{ + os.CacheRule{ + Name: "default", + TTL: 3600, + }, + }, + Restrictions: []os.Restriction{}, + FlavorID: "europe", + Status: "deployed", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1", + Rel: "self", + }, + gophercloud.Link{ + Href: "myothersite.com.poppycdn.net", + Rel: "access_url", + }, + gophercloud.Link{ + Href: "https://www.poppycdn.io/v1.0/flavors/europe", + Rel: "flavor", + }, + }, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.HandleCreateCDNServiceSuccessfully(t) + + createOpts := os.CreateOpts{ + Name: "mywebsite.com", + Domains: []os.Domain{ + os.Domain{ + Domain: "www.mywebsite.com", + }, + os.Domain{ + Domain: "blog.mywebsite.com", + }, + }, + Origins: []os.Origin{ + os.Origin{ + Origin: "mywebsite.com", + Port: 80, + SSL: false, + }, + }, + Restrictions: []os.Restriction{ + os.Restriction{ + Name: "website only", + Rules: []os.RestrictionRule{ + os.RestrictionRule{ + Name: "mywebsite.com", + Referrer: "www.mywebsite.com", + }, + }, + }, + }, + Caching: []os.CacheRule{ + os.CacheRule{ + Name: "default", + TTL: 3600, + }, + }, + FlavorID: "cdn", + } + + expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" + actual, err := Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, expected, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.HandleGetCDNServiceSuccessfully(t) + + expected := &os.Service{ + ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Name: "mywebsite.com", + Domains: []os.Domain{ + os.Domain{ + Domain: "www.mywebsite.com", + Protocol: "http", + }, + }, + Origins: []os.Origin{ + os.Origin{ + Origin: "mywebsite.com", + Port: 80, + SSL: false, + }, + }, + Caching: []os.CacheRule{ + os.CacheRule{ + Name: "default", + TTL: 3600, + }, + os.CacheRule{ + Name: "home", + TTL: 17200, + Rules: []os.TTLRule{ + os.TTLRule{ + Name: "index", + RequestURL: "/index.htm", + }, + }, + }, + os.CacheRule{ + Name: "images", + TTL: 12800, + Rules: []os.TTLRule{ + os.TTLRule{ + Name: "images", + RequestURL: "*.png", + }, + }, + }, + }, + Restrictions: []os.Restriction{ + os.Restriction{ + Name: "website only", + Rules: []os.RestrictionRule{ + os.RestrictionRule{ + Name: "mywebsite.com", + Referrer: "www.mywebsite.com", + }, + }, + }, + }, + FlavorID: "cdn", + Status: "deployed", + Errors: []os.Error{}, + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", + Rel: "self", + }, + gophercloud.Link{ + Href: "blog.mywebsite.com.cdn1.raxcdn.com", + Rel: "access_url", + }, + gophercloud.Link{ + Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn", + Rel: "flavor", + }, + }, + } + + actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestSuccessfulUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.HandleUpdateCDNServiceSuccessfully(t) + + expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" + ops := []os.Patch{ + // Append a single Domain + os.Append{Value: os.Domain{Domain: "appended.mocksite4.com"}}, + // Insert a single Domain + os.Insertion{ + Index: 4, + Value: os.Domain{Domain: "inserted.mocksite4.com"}, + }, + // Bulk addition + os.Append{ + Value: os.DomainList{ + os.Domain{Domain: "bulkadded1.mocksite4.com"}, + os.Domain{Domain: "bulkadded2.mocksite4.com"}, + }, + }, + // Replace a single Origin + os.Replacement{ + Index: 2, + Value: os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false}, + }, + // Bulk replace Origins + os.Replacement{ + Index: 0, // Ignored + Value: os.OriginList{ + os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false}, + os.Origin{Origin: "55.44.33.22", Port: 443, SSL: true}, + }, + }, + // Remove a single CacheRule + os.Removal{ + Index: 8, + Path: os.PathCaching, + }, + // Bulk removal + os.Removal{ + All: true, + Path: os.PathCaching, + }, + // Service name replacement + os.NameReplacement{ + NewName: "differentServiceName", + }, + } + + actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, expected, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.HandleDeleteCDNServiceSuccessfully(t) + + err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/doc.go new file mode 100644 index 00000000000..ee6e2a54fc9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/cdn/v1/services/doc.go @@ -0,0 +1,7 @@ +// Package services provides information and interaction with the services API +// resource in the Rackspace CDN service. This API resource allows for +// listing, creating, updating, retrieving, and deleting services. +// +// A service represents an application that has its content cached to the edge +// nodes. +package services diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go index 5f739a8b899..45199a40326 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go @@ -154,3 +154,36 @@ func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoi return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil } + +// NewLBV1 creates a ServiceClient that can be used to access the Rackspace +// Cloud Load Balancer v1 API. +func NewLBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("rax:load-balancer") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewNetworkV2 creates a ServiceClient that can be used to access the Rackspace +// Networking v2 API. +func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("network") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewCDNV1 creates a ServiceClient that may be used to access the Rackspace v1 +// CDN service. +func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("rax:cdn") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go index b6dca937caa..894f916f675 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go @@ -1,4 +1,5 @@ // +build fixtures + package flavors import ( diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go index c46d196cbf2..ccfbdc6a1e4 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go @@ -1,4 +1,5 @@ // +build fixtures + package images import ( diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go index 4c7b24909aa..173868edf1b 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go @@ -16,6 +16,11 @@ func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.Cre return os.Create(client, opts) } +// Update requests an existing server to be updated with the supplied options. +func Update(client *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(client, id, opts) +} + // Delete requests that a server previously provisioned be removed from your account. func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { return os.Delete(client, id) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go index 7f414040f7e..c3d9cc0897e 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go @@ -77,6 +77,28 @@ func TestGetServer(t *testing.T) { th.CheckDeepEquals(t, &GophercloudServer, actual) } +func TestUpdateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", 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, `{ "server": { "name": "test-server-updated" } }`) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, UpdateOutput) + }) + + opts := os.UpdateOpts{ + Name: "test-server-updated", + } + actual, err := Update(client.ServiceClient(), "8c65cb68-0681-4c30-bc88-6b83a8a26aee", opts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GophercloudUpdatedServer, actual) +} + func TestChangeAdminPassword(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go index b22a28998d1..75cccd04181 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go @@ -215,6 +215,77 @@ const GetOutput = ` } ` +// UpdateOutput is the recorded output of a Rackspace servers.Update request. +const UpdateOutput = ` +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "accessIPv4": "1.2.4.8", + "accessIPv6": "2001:4800:6666:105:2a0f:c056:f594:7777", + "addresses": { + "private": [ + { + "addr": "10.20.40.80", + "version": 4 + } + ], + "public": [ + { + "addr": "1.2.4.8", + "version": 4 + }, + { + "addr": "2001:4800:6666:105:2a0f:c056:f594:7777", + "version": 6 + } + ] + }, + "created": "2014-10-21T14:42:16Z", + "flavor": { + "id": "performance1-1", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1", + "rel": "bookmark" + } + ] + }, + "hostId": "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7", + "id": "8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "image": { + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark" + } + ] + }, + "key_name": null, + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "test-server-updated", + "progress": 100, + "status": "ACTIVE", + "tenant_id": "111111", + "updated": "2014-10-21T14:42:57Z", + "user_id": "14ae7bb21d81423694655f4dd30f2930" + } +} +` + // CreateOutput contains a sample of Rackspace's response to a Create call. const CreateOutput = ` { @@ -428,6 +499,70 @@ var GophercloudServer = os.Server{ AdminPass: "", } +// GophercloudUpdatedServer is the expected result from parsing UpdateOutput. +var GophercloudUpdatedServer = os.Server{ + ID: "8c65cb68-0681-4c30-bc88-6b83a8a26aee", + Name: "test-server-updated", + TenantID: "111111", + UserID: "14ae7bb21d81423694655f4dd30f2930", + HostID: "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7", + Updated: "2014-10-21T14:42:57Z", + Created: "2014-10-21T14:42:16Z", + AccessIPv4: "1.2.4.8", + AccessIPv6: "2001:4800:6666:105:2a0f:c056:f594:7777", + Progress: 100, + Status: "ACTIVE", + Image: map[string]interface{}{ + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "performance1-1", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "addr": "10.20.40.80", + "version": float64(4.0), + }, + }, + "public": []interface{}{ + map[string]interface{}{ + "addr": "2001:4800:6666:105:2a0f:c056:f594:7777", + "version": float64(6.0), + }, + map[string]interface{}{ + "addr": "1.2.4.8", + "version": float64(4.0), + }, + }, + }, + Metadata: map[string]interface{}{}, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "self", + }, + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", + "rel": "bookmark", + }, + }, + KeyName: "", + AdminPass: "", +} + // CreatedServer is the partial Server struct that can be parsed from CreateOutput. var CreatedServer = os.Server{ ID: "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go index 884b9cb131e..809183ec7c0 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go @@ -43,6 +43,10 @@ type CreateOpts struct { // ConfigDrive [optional] enables metadata injection through a configuration drive. ConfigDrive bool + // AdminPass [optional] sets the root user password. If not set, a randomly-generated + // password will be created and returned in the response. + AdminPass string + // Rackspace-specific extensions begin here. // KeyPair [optional] specifies the name of the SSH KeyPair to be injected into the newly launched @@ -72,6 +76,7 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { Metadata: opts.Metadata, Personality: opts.Personality, ConfigDrive: opts.ConfigDrive, + AdminPass: opts.AdminPass, } drive := diskconfig.CreateOptsExt{ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate.go new file mode 100644 index 00000000000..a6c01e4f2d9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate.go @@ -0,0 +1,53 @@ +package roles + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles" +) + +// List is the operation responsible for listing all available global roles +// that a user can adopt. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client) +} + +// AddUserRole is the operation responsible for assigning a particular role to +// a user. This is confined to the scope of the user's tenant - so the tenant +// ID is a required argument. +func AddUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult { + var result UserRoleResult + + _, result.Err = perigee.Request("PUT", userRoleURL(client, userID, roleID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200, 201}, + }) + + return result +} + +// DeleteUserRole is the operation responsible for deleting a particular role +// from a user. This is confined to the scope of the user's tenant - so the +// tenant ID is a required argument. +func DeleteUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult { + var result UserRoleResult + + _, result.Err = perigee.Request("DELETE", userRoleURL(client, userID, roleID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{204}, + }) + + return result +} + +// UserRoleResult represents the result of either an AddUserRole or +// a DeleteUserRole operation. +type UserRoleResult struct { + gophercloud.ErrResult +} + +func userRoleURL(c *gophercloud.ServiceClient, userID, roleID string) string { + return c.ServiceURL(os.UserPath, userID, os.RolePath, os.ExtPath, roleID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate_test.go new file mode 100644 index 00000000000..fcee97d0bc3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate_test.go @@ -0,0 +1,66 @@ +package roles + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListRoleResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractRoles(page) + if err != nil { + t.Errorf("Failed to extract users: %v", err) + return false, err + } + + expected := []os.Role{ + os.Role{ + ID: "123", + Name: "compute:admin", + Description: "Nova Administrator", + ServiceID: "cke5372ebabeeabb70a0e702a4626977x4406e5", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestAddUserRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockAddUserRoleResponse(t) + + err := AddUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr() + + th.AssertNoErr(t, err) +} + +func TestDeleteUserRole(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteUserRoleResponse(t) + + err := DeleteUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr() + + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/fixtures.go new file mode 100644 index 00000000000..5f22d0f6424 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/fixtures.go @@ -0,0 +1,49 @@ +package roles + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func MockListRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/OS-KSADM/roles", 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, ` +{ + "roles": [ + { + "id": "123", + "name": "compute:admin", + "description": "Nova Administrator", + "serviceId": "cke5372ebabeeabb70a0e702a4626977x4406e5" + } + ] +} + `) + }) +} + +func MockAddUserRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusCreated) + }) +} + +func MockDeleteUserRoleResponse(t *testing.T) { + th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", 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) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate.go new file mode 100644 index 00000000000..ae2acde643c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate.go @@ -0,0 +1,145 @@ +package users + +import ( + "errors" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/users" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a pager that allows traversal over a collection of users. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client) +} + +// CommonOpts are the options which are shared between CreateOpts and +// UpdateOpts +type CommonOpts struct { + // Required. The username to assign to the user. When provided, the username + // must: + // - start with an alphabetical (A-Za-z) character + // - have a minimum length of 1 character + // + // The username may contain upper and lowercase characters, as well as any of + // the following special character: . - @ _ + Username string + + // Required. Email address for the user account. + Email string + + // Required. Indicates whether the user can authenticate after the user + // account is created. If no value is specified, the default value is true. + Enabled os.EnabledState + + // Optional. The password to assign to the user. If provided, the password + // must: + // - start with an alphabetical (A-Za-z) character + // - have a minimum length of 8 characters + // - contain at least one uppercase character, one lowercase character, and + // one numeric character. + // + // The password may contain any of the following special characters: . - @ _ + Password string +} + +// CreateOpts represents the options needed when creating new users. +type CreateOpts CommonOpts + +// ToUserCreateMap assembles a request body based on the contents of a CreateOpts. +func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) { + m := make(map[string]interface{}) + + if opts.Username == "" { + return m, errors.New("Username is a required field") + } + if opts.Enabled == nil { + return m, errors.New("Enabled is a required field") + } + if opts.Email == "" { + return m, errors.New("Email is a required field") + } + + if opts.Username != "" { + m["username"] = opts.Username + } + if opts.Email != "" { + m["email"] = opts.Email + } + if opts.Enabled != nil { + m["enabled"] = opts.Enabled + } + if opts.Password != "" { + m["OS-KSADM:password"] = opts.Password + } + + return map[string]interface{}{"user": m}, nil +} + +// Create is the operation responsible for creating new users. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult { + return CreateResult{os.Create(client, opts)} +} + +// Get requests details on a single user, either by ID. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + return GetResult{os.Get(client, id)} +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToUserUpdateMap() map[string]interface{} +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts CommonOpts + +// ToUserUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} { + m := make(map[string]interface{}) + + if opts.Username != "" { + m["username"] = opts.Username + } + if opts.Enabled != nil { + m["enabled"] = &opts.Enabled + } + if opts.Email != "" { + m["email"] = opts.Email + } + + return map[string]interface{}{"user": m} +} + +// Update is the operation responsible for updating exist users by their UUID. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var result UpdateResult + + _, result.Err = perigee.Request("POST", os.ResourceURL(client, id), perigee.Options{ + Results: &result.Body, + ReqBody: opts.ToUserUpdateMap(), + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} + +// Delete is the operation responsible for permanently deleting an API user. +func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(client, id) +} + +// ResetAPIKey resets the User's API key. +func ResetAPIKey(client *gophercloud.ServiceClient, id string) ResetAPIKeyResult { + var result ResetAPIKeyResult + + _, result.Err = perigee.Request("POST", resetAPIKeyURL(client, id), perigee.Options{ + Results: &result.Body, + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate_test.go new file mode 100644 index 00000000000..62faf0c5bec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate_test.go @@ -0,0 +1,111 @@ +package users + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/identity/v2/users" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListResponse(t) + + count := 0 + + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + users, err := os.ExtractUsers(page) + + th.AssertNoErr(t, err) + th.AssertEquals(t, "u1000", users[0].ID) + th.AssertEquals(t, "u1001", users[1].ID) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateUser(t) + + opts := CreateOpts{ + Username: "new_user", + Enabled: os.Disabled, + Email: "new_user@foo.com", + Password: "foo", + } + + user, err := Create(client.ServiceClient(), opts).Extract() + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "123456", user.ID) + th.AssertEquals(t, "5830280", user.DomainID) + th.AssertEquals(t, "DFW", user.DefaultRegion) +} + +func TestGetUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetUser(t) + + user, err := Get(client.ServiceClient(), "new_user").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, true, user.Enabled) + th.AssertEquals(t, true, user.MultiFactorEnabled) +} + +func TestUpdateUser(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateUser(t) + + id := "c39e3de9be2d4c779f1dfd6abacc176d" + + opts := UpdateOpts{ + Enabled: os.Enabled, + Email: "new_email@foo.com", + } + + user, err := Update(client.ServiceClient(), id, opts).Extract() + + th.AssertNoErr(t, err) + + th.AssertEquals(t, true, user.Enabled) + th.AssertEquals(t, "new_email@foo.com", user.Email) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteUser(t) + + res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d") + th.AssertNoErr(t, res.Err) +} + +func TestResetAPIKey(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockResetAPIKey(t) + + apiKey, err := ResetAPIKey(client.ServiceClient(), "99").Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, "joesmith", apiKey.Username) + th.AssertEquals(t, "mooH1eiLahd5ahYood7r", apiKey.APIKey) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/fixtures.go new file mode 100644 index 00000000000..973f39ea8c9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/fixtures.go @@ -0,0 +1,154 @@ +package users + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func mockListResponse(t *testing.T) { + th.Mux.HandleFunc("/users", 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, ` +{ + "users":[ + { + "id": "u1000", + "username": "jqsmith", + "email": "john.smith@example.org", + "enabled": true + }, + { + "id": "u1001", + "username": "jqsmith", + "email": "jane.smith@example.org", + "enabled": true + } + ] +} + `) + }) +} + +func mockCreateUser(t *testing.T) { + th.Mux.HandleFunc("/users", 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, ` +{ + "user": { + "username": "new_user", + "enabled": false, + "email": "new_user@foo.com", + "OS-KSADM:password": "foo" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "RAX-AUTH:defaultRegion": "DFW", + "RAX-AUTH:domainId": "5830280", + "id": "123456", + "username": "new_user", + "email": "new_user@foo.com", + "enabled": false + } +} +`) + }) +} + +func mockGetUser(t *testing.T) { + th.Mux.HandleFunc("/users/new_user", 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, ` +{ + "user": { + "RAX-AUTH:defaultRegion": "DFW", + "RAX-AUTH:domainId": "5830280", + "RAX-AUTH:multiFactorEnabled": "true", + "id": "c39e3de9be2d4c779f1dfd6abacc176d", + "username": "jqsmith", + "email": "john.smith@example.org", + "enabled": true + } +} +`) + }) +} + +func mockUpdateUser(t *testing.T) { + th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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, ` +{ + "user": { + "email": "new_email@foo.com", + "enabled": true + } +} +`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "user": { + "RAX-AUTH:defaultRegion": "DFW", + "RAX-AUTH:domainId": "5830280", + "RAX-AUTH:multiFactorEnabled": "true", + "id": "123456", + "username": "jqsmith", + "email": "new_email@foo.com", + "enabled": true + } +} +`) + }) +} + +func mockDeleteUser(t *testing.T) { + th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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) + }) +} + +func mockResetAPIKey(t *testing.T) { + th.Mux.HandleFunc("/users/99/OS-KSADM/credentials/RAX-KSKEY:apiKeyCredentials/RAX-AUTH/reset", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "RAX-KSKEY:apiKeyCredentials": { + "username": "joesmith", + "apiKey": "mooH1eiLahd5ahYood7r" + } +}`) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/results.go new file mode 100644 index 00000000000..6936ecba84e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/results.go @@ -0,0 +1,129 @@ +package users + +import ( + "strconv" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/users" + + "github.com/mitchellh/mapstructure" +) + +// User represents a user resource that exists on the API. +type User struct { + // The UUID for this user. + ID string + + // The human name for this user. + Name string + + // The username for this user. + Username string + + // Indicates whether the user is enabled (true) or disabled (false). + Enabled bool + + // The email address for this user. + Email string + + // The ID of the tenant to which this user belongs. + TenantID string `mapstructure:"tenant_id"` + + // Specifies the default region for the user account. This value is inherited + // from the user administrator when the account is created. + DefaultRegion string `mapstructure:"RAX-AUTH:defaultRegion"` + + // Identifies the domain that contains the user account. This value is + // inherited from the user administrator when the account is created. + DomainID string `mapstructure:"RAX-AUTH:domainId"` + + // The password value that the user needs for authentication. If the Add user + // request included a password value, this attribute is not included in the + // response. + Password string `mapstructure:"OS-KSADM:password"` + + // Indicates whether the user has enabled multi-factor authentication. + MultiFactorEnabled bool `mapstructure:"RAX-AUTH:multiFactorEnabled"` +} + +// CreateResult represents the result of a Create operation +type CreateResult struct { + os.CreateResult +} + +// GetResult represents the result of a Get operation +type GetResult struct { + os.GetResult +} + +// UpdateResult represents the result of an Update operation +type UpdateResult struct { + os.UpdateResult +} + +func commonExtract(resp interface{}, err error) (*User, error) { + if err != nil { + return nil, err + } + + var respStruct struct { + User *User `json:"user"` + } + + // Since the API returns a string instead of a bool, we need to hack the JSON + json := resp.(map[string]interface{}) + user := json["user"].(map[string]interface{}) + if s, ok := user["RAX-AUTH:multiFactorEnabled"].(string); ok && s != "" { + if b, err := strconv.ParseBool(s); err == nil { + user["RAX-AUTH:multiFactorEnabled"] = b + } + } + + err = mapstructure.Decode(json, &respStruct) + + return respStruct.User, err +} + +// Extract will get the Snapshot object out of the GetResult object. +func (r GetResult) Extract() (*User, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Snapshot object out of the CreateResult object. +func (r CreateResult) Extract() (*User, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Snapshot object out of the UpdateResult object. +func (r UpdateResult) Extract() (*User, error) { + return commonExtract(r.Body, r.Err) +} + +// ResetAPIKeyResult represents the server response to the ResetAPIKey method. +type ResetAPIKeyResult struct { + gophercloud.Result +} + +// ResetAPIKeyValue represents an API Key that has been reset. +type ResetAPIKeyValue struct { + // The Username for this API Key reset. + Username string `mapstructure:"username"` + + // The new API Key for this user. + APIKey string `mapstructure:"apiKey"` +} + +// Extract will get the Error or ResetAPIKeyValue object out of the ResetAPIKeyResult object. +func (r ResetAPIKeyResult) Extract() (*ResetAPIKeyValue, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + ResetAPIKeyValue ResetAPIKeyValue `mapstructure:"RAX-KSKEY:apiKeyCredentials"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.ResetAPIKeyValue, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/urls.go new file mode 100644 index 00000000000..bc1aaefb022 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/urls.go @@ -0,0 +1,7 @@ +package users + +import "github.com/rackspace/gophercloud" + +func resetAPIKeyURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("users", id, "OS-KSADM", "credentials", "RAX-KSKEY:apiKeyCredentials", "RAX-AUTH", "reset") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/doc.go new file mode 100644 index 00000000000..42325fe83d5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/doc.go @@ -0,0 +1,12 @@ +/* +Package acl provides information and interaction with the access lists feature +of the Rackspace Cloud Load Balancer service. + +The access list management feature allows fine-grained network access controls +to be applied to the load balancer's virtual IP address. A single IP address, +multiple IP addresses, or entire network subnets can be added. Items that are +configured with the ALLOW type always takes precedence over items with the DENY +type. To reject traffic from all items except for those with the ALLOW type, +add a networkItem with an address of "0.0.0.0/0" and a DENY type. +*/ +package acl diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/fixtures.go new file mode 100644 index 00000000000..e3c941c81ba --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/fixtures.go @@ -0,0 +1,109 @@ +package acl + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(lbID int) string { + return "/loadbalancers/" + strconv.Itoa(lbID) + "/accesslist" +} + +func mockListResponse(t *testing.T, id int) { + th.Mux.HandleFunc(_rootURL(id), 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, ` +{ + "accessList": [ + { + "address": "206.160.163.21", + "id": 21, + "type": "DENY" + }, + { + "address": "206.160.163.22", + "id": 22, + "type": "DENY" + }, + { + "address": "206.160.163.23", + "id": 23, + "type": "DENY" + }, + { + "address": "206.160.163.24", + "id": 24, + "type": "DENY" + } + ] +} + `) + }) +} + +func mockCreateResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "accessList": [ + { + "address": "206.160.163.21", + "type": "DENY" + }, + { + "address": "206.160.165.11", + "type": "DENY" + } + ] +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteAllResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + r.ParseForm() + + for k, v := range ids { + fids := r.Form["id"] + th.AssertEquals(t, strconv.Itoa(v), fids[k]) + } + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteResponse(t *testing.T, lbID, networkID int) { + th.Mux.HandleFunc(_rootURL(lbID)+"/"+strconv.Itoa(networkID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests.go new file mode 100644 index 00000000000..e1e92ac6319 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests.go @@ -0,0 +1,127 @@ +package acl + +import ( + "errors" + "fmt" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List is the operation responsible for returning a paginated collection of +// network items that define a load balancer's access list. +func List(client *gophercloud.ServiceClient, lbID int) pagination.Pager { + url := rootURL(client, lbID) + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessListPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder is the interface responsible for generating the JSON +// for a Create operation. +type CreateOptsBuilder interface { + ToAccessListCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is a slice of CreateOpt structs, that allow the user to create +// multiple nodes in a single operation (one node per CreateOpt). +type CreateOpts []CreateOpt + +// CreateOpt represents the options to create a single node. +type CreateOpt struct { + // Required - the IP address or CIDR for item to add to access list. + Address string + + // Required - the type of the node. Either ALLOW or DENY. + Type Type +} + +// ToAccessListCreateMap converts a slice of options into a map that can be +// used for the JSON. +func (opts CreateOpts) ToAccessListCreateMap() (map[string]interface{}, error) { + type itemMap map[string]interface{} + items := []itemMap{} + + for k, v := range opts { + if v.Address == "" { + return itemMap{}, fmt.Errorf("Address is a required attribute, none provided for %d CreateOpt element", k) + } + if v.Type != ALLOW && v.Type != DENY { + return itemMap{}, fmt.Errorf("Type must be ALLOW or DENY") + } + + item := make(itemMap) + item["address"] = v.Address + item["type"] = v.Type + + items = append(items, item) + } + + return itemMap{"accessList": items}, nil +} + +// Create is the operation responsible for adding network items to the access +// rules for a particular load balancer. If network items already exist, the +// new item will be appended. A single IP address or subnet range is considered +// unique and cannot be duplicated. +func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToAccessListCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", rootURL(client, loadBalancerID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// BulkDelete will delete multiple network items from a load balancer's access +// list in a single operation. +func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, itemIDs []int) DeleteResult { + var res DeleteResult + + if len(itemIDs) > 10 || len(itemIDs) == 0 { + res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 item IDs") + return res + } + + url := rootURL(c, loadBalancerID) + url += gophercloud.IDSliceToQueryString("id", itemIDs) + + _, res.Err = perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// Delete will remove a single network item from a load balancer's access list. +func Delete(c *gophercloud.ServiceClient, lbID, itemID int) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, itemID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} + +// DeleteAll will delete the entire contents of a load balancer's access list, +// effectively resetting it and allowing all traffic. +func DeleteAll(c *gophercloud.ServiceClient, lbID int) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests_test.go new file mode 100644 index 00000000000..c4961a3dd8f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests_test.go @@ -0,0 +1,91 @@ +package acl + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + lbID = 12345 + itemID1 = 67890 + itemID2 = 67891 +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListResponse(t, lbID) + + count := 0 + + err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractAccessList(page) + th.AssertNoErr(t, err) + + expected := AccessList{ + NetworkItem{Address: "206.160.163.21", ID: 21, Type: DENY}, + NetworkItem{Address: "206.160.163.22", ID: 22, Type: DENY}, + NetworkItem{Address: "206.160.163.23", ID: 23, Type: DENY}, + NetworkItem{Address: "206.160.163.24", ID: 24, Type: DENY}, + } + + 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() + + mockCreateResponse(t, lbID) + + opts := CreateOpts{ + CreateOpt{Address: "206.160.163.21", Type: DENY}, + CreateOpt{Address: "206.160.165.11", Type: DENY}, + } + + err := Create(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + ids := []int{itemID1, itemID2} + + mockBatchDeleteResponse(t, lbID, ids) + + err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID, itemID1) + + err := Delete(client.ServiceClient(), lbID, itemID1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDeleteAll(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteAllResponse(t, lbID) + + err := DeleteAll(client.ServiceClient(), lbID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/results.go new file mode 100644 index 00000000000..9ea5ea2f4b7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/results.go @@ -0,0 +1,72 @@ +package acl + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// AccessList represents the rules of network access to a particular load +// balancer. +type AccessList []NetworkItem + +// NetworkItem describes how an IP address or entire subnet may interact with a +// load balancer. +type NetworkItem struct { + // The IP address or subnet (CIDR) that defines the network item. + Address string + + // The numeric unique ID for this item. + ID int + + // Either ALLOW or DENY. + Type Type +} + +// Type defines how an item may connect to the load balancer. +type Type string + +// Convenience consts. +const ( + ALLOW Type = "ALLOW" + DENY Type = "DENY" +) + +// AccessListPage is the page returned by a pager for traversing over a +// collection of network items in an access list. +type AccessListPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an AccessListPage struct is empty. +func (p AccessListPage) IsEmpty() (bool, error) { + is, err := ExtractAccessList(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractAccessList accepts a Page struct, specifically an AccessListPage +// struct, and extracts the elements into a slice of NetworkItem structs. In +// other words, a generic collection is mapped into a relevant slice. +func ExtractAccessList(page pagination.Page) (AccessList, error) { + var resp struct { + List AccessList `mapstructure:"accessList" json:"accessList"` + } + + err := mapstructure.Decode(page.(AccessListPage).Body, &resp) + + return resp.List, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + gophercloud.ErrResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/urls.go new file mode 100644 index 00000000000..e373fa1d809 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/urls.go @@ -0,0 +1,20 @@ +package acl + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + aclPath = "accesslist" +) + +func resourceURL(c *gophercloud.ServiceClient, lbID, networkID int) string { + return c.ServiceURL(path, strconv.Itoa(lbID), aclPath, strconv.Itoa(networkID)) +} + +func rootURL(c *gophercloud.ServiceClient, lbID int) string { + return c.ServiceURL(path, strconv.Itoa(lbID), aclPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/doc.go new file mode 100644 index 00000000000..05f0032859b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/doc.go @@ -0,0 +1,44 @@ +/* +Package lbs provides information and interaction with the Load Balancer API +resource for the Rackspace Cloud Load Balancer service. + +A load balancer is a logical device which belongs to a cloud account. It is +used to distribute workloads between multiple back-end systems or services, +based on the criteria defined as part of its configuration. This configuration +is defined using the Create operation, and can be updated with Update. + +To conserve IPv4 address space, it is highly recommended that you share Virtual +IPs between load balancers. If you have at least one load balancer, you may +create subsequent ones that share a single virtual IPv4 and/or a single IPv6 by +passing in a virtual IP ID to the Update operation (instead of a type). This +feature is also highly desirable if you wish to load balance both an insecure +and secure protocol using one IP or DNS name. In order to share a virtual IP, +each Load Balancer must utilize a unique port. + +All load balancers have a Status attribute that shows the current configuration +status of the device. This status is immutable by the caller and is updated +automatically based on state changes within the service. When a load balancer +is first created, it is placed into a BUILD state while the configuration is +being generated and applied based on the request. Once the configuration is +applied and finalized, it is in an ACTIVE status. In the event of a +configuration change or update, the status of the load balancer changes to +PENDING_UPDATE to signify configuration changes are in progress but have not yet +been finalized. Load balancers in a SUSPENDED status are configured to reject +traffic and do not forward requests to back-end nodes. + +An HTTP load balancer has the X-Forwarded-For (XFF) HTTP header set by default. +This header contains the originating IP address of a client connecting to a web +server through an HTTP proxy or load balancer, which many web applications are +already designed to use when determining the source address for a request. + +It also includes the X-Forwarded-Proto (XFP) HTTP header, which has been added +for identifying the originating protocol of an HTTP request as "http" or +"https" depending on which protocol the client requested. This is useful when +using SSL termination. + +Finally, it also includes the X-Forwarded-Port HTTP header, which has been +added for being able to generate secure URLs containing the specified port. +This header, along with the X-Forwarded-For header, provides the needed +information to the underlying application servers. +*/ +package lbs diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/fixtures.go new file mode 100644 index 00000000000..6325310dbbb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/fixtures.go @@ -0,0 +1,584 @@ +package lbs + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func mockListLBResponse(t *testing.T) { + th.Mux.HandleFunc("/loadbalancers", 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, ` +{ + "loadBalancers":[ + { + "name":"lb-site1", + "id":71, + "protocol":"HTTP", + "port":80, + "algorithm":"RANDOM", + "status":"ACTIVE", + "nodeCount":3, + "virtualIps":[ + { + "id":403, + "address":"206.55.130.1", + "type":"PUBLIC", + "ipVersion":"IPV4" + } + ], + "created":{ + "time":"2010-11-30T03:23:42Z" + }, + "updated":{ + "time":"2010-11-30T03:23:44Z" + } + }, + { + "name":"lb-site2", + "id":72, + "created":{ + "time":"2011-11-30T03:23:42Z" + }, + "updated":{ + "time":"2011-11-30T03:23:44Z" + } + }, + { + "name":"lb-site3", + "id":73, + "created":{ + "time":"2012-11-30T03:23:42Z" + }, + "updated":{ + "time":"2012-11-30T03:23:44Z" + } + } + ] +} + `) + }) +} + +func mockCreateLBResponse(t *testing.T) { + th.Mux.HandleFunc("/loadbalancers", 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, ` +{ + "loadBalancer": { + "name": "a-new-loadbalancer", + "port": 80, + "protocol": "HTTP", + "virtualIps": [ + { + "id": 2341 + }, + { + "id": 900001 + } + ], + "nodes": [ + { + "address": "10.1.1.1", + "port": 80, + "condition": "ENABLED" + } + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "loadBalancer": { + "name": "a-new-loadbalancer", + "id": 144, + "protocol": "HTTP", + "halfClosed": false, + "port": 83, + "algorithm": "RANDOM", + "status": "BUILD", + "timeout": 30, + "cluster": { + "name": "ztm-n01.staging1.lbaas.rackspace.net" + }, + "nodes": [ + { + "address": "10.1.1.1", + "id": 653, + "port": 80, + "status": "ONLINE", + "condition": "ENABLED", + "weight": 1 + } + ], + "virtualIps": [ + { + "address": "206.10.10.210", + "id": 39, + "type": "PUBLIC", + "ipVersion": "IPV4" + }, + { + "address": "2001:4801:79f1:0002:711b:be4c:0000:0021", + "id": 900001, + "type": "PUBLIC", + "ipVersion": "IPV6" + } + ], + "created": { + "time": "2010-11-30T03:23:42Z" + }, + "updated": { + "time": "2010-11-30T03:23:44Z" + }, + "connectionLogging": { + "enabled": false + } + } +} + `) + }) +} + +func mockBatchDeleteLBResponse(t *testing.T, ids []int) { + th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + r.ParseForm() + + for k, v := range ids { + fids := r.Form["id"] + th.AssertEquals(t, strconv.Itoa(v), fids[k]) + } + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteLBResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockGetLBResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), 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, ` +{ + "loadBalancer": { + "id": 2000, + "name": "sample-loadbalancer", + "protocol": "HTTP", + "port": 80, + "algorithm": "RANDOM", + "status": "ACTIVE", + "timeout": 30, + "connectionLogging": { + "enabled": true + }, + "virtualIps": [ + { + "id": 1000, + "address": "206.10.10.210", + "type": "PUBLIC", + "ipVersion": "IPV4" + } + ], + "nodes": [ + { + "id": 1041, + "address": "10.1.1.1", + "port": 80, + "condition": "ENABLED", + "status": "ONLINE" + }, + { + "id": 1411, + "address": "10.1.1.2", + "port": 80, + "condition": "ENABLED", + "status": "ONLINE" + } + ], + "sessionPersistence": { + "persistenceType": "HTTP_COOKIE" + }, + "connectionThrottle": { + "maxConnections": 100 + }, + "cluster": { + "name": "c1.dfw1" + }, + "created": { + "time": "2010-11-30T03:23:42Z" + }, + "updated": { + "time": "2010-11-30T03:23:44Z" + }, + "sourceAddresses": { + "ipv6Public": "2001:4801:79f1:1::1/64", + "ipv4Servicenet": "10.0.0.0", + "ipv4Public": "10.12.99.28" + } + } +} + `) + }) +} + +func mockUpdateLBResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), 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, ` +{ + "loadBalancer": { + "name": "a-new-loadbalancer", + "protocol": "TCP", + "halfClosed": true, + "algorithm": "RANDOM", + "port": 8080, + "timeout": 100, + "httpsRedirect": false + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockListProtocolsResponse(t *testing.T) { + th.Mux.HandleFunc("/loadbalancers/protocols", 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, ` +{ + "protocols": [ + { + "name": "DNS_TCP", + "port": 53 + }, + { + "name": "DNS_UDP", + "port": 53 + }, + { + "name": "FTP", + "port": 21 + }, + { + "name": "HTTP", + "port": 80 + }, + { + "name": "HTTPS", + "port": 443 + }, + { + "name": "IMAPS", + "port": 993 + }, + { + "name": "IMAPv4", + "port": 143 + }, + { + "name": "LDAP", + "port": 389 + }, + { + "name": "LDAPS", + "port": 636 + }, + { + "name": "MYSQL", + "port": 3306 + }, + { + "name": "POP3", + "port": 110 + }, + { + "name": "POP3S", + "port": 995 + }, + { + "name": "SMTP", + "port": 25 + }, + { + "name": "TCP", + "port": 0 + }, + { + "name": "TCP_CLIENT_FIRST", + "port": 0 + }, + { + "name": "UDP", + "port": 0 + }, + { + "name": "UDP_STREAM", + "port": 0 + }, + { + "name": "SFTP", + "port": 22 + }, + { + "name": "TCP_STREAM", + "port": 0 + } + ] +} + `) + }) +} + +func mockListAlgorithmsResponse(t *testing.T) { + th.Mux.HandleFunc("/loadbalancers/algorithms", 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, ` +{ + "algorithms": [ + { + "name": "LEAST_CONNECTIONS" + }, + { + "name": "RANDOM" + }, + { + "name": "ROUND_ROBIN" + }, + { + "name": "WEIGHTED_LEAST_CONNECTIONS" + }, + { + "name": "WEIGHTED_ROUND_ROBIN" + } + ] +} + `) + }) +} + +func mockGetLoggingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", 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, ` +{ + "connectionLogging": { + "enabled": true + } +} + `) + }) +} + +func mockEnableLoggingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", 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, ` +{ + "connectionLogging":{ + "enabled":true + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDisableLoggingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", 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, ` +{ + "connectionLogging":{ + "enabled":false + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockGetErrorPageResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", 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, ` +{ + "errorpage": { + "content": "DEFAULT ERROR PAGE" + } +} + `) + }) +} + +func mockSetErrorPageResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", 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, ` +{ + "errorpage": { + "content": "New error page" + } +} + `) + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "errorpage": { + "content": "New error page" + } +} + `) + }) +} + +func mockDeleteErrorPageResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", 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) + }) +} + +func mockGetStatsResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/stats", 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, ` +{ + "connectTimeOut": 10, + "connectError": 20, + "connectFailure": 30, + "dataTimedOut": 40, + "keepAliveTimedOut": 50, + "maxConn": 60, + "currentConn": 40, + "connectTimeOutSsl": 10, + "connectErrorSsl": 20, + "connectFailureSsl": 30, + "dataTimedOutSsl": 40, + "keepAliveTimedOutSsl": 50, + "maxConnSsl": 60, + "currentConnSsl": 40 +} + `) + }) +} + +func mockGetCachingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", 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, ` +{ + "contentCaching": { + "enabled": true + } +} + `) + }) +} + +func mockEnableCachingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", 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, ` +{ + "contentCaching":{ + "enabled":true + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDisableCachingResponse(t *testing.T, id int) { + th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", 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, ` +{ + "contentCaching":{ + "enabled":false + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests.go new file mode 100644 index 00000000000..342f10790ec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests.go @@ -0,0 +1,574 @@ +package lbs + +import ( + "errors" + + "github.com/mitchellh/mapstructure" + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/acl" + "github.com/rackspace/gophercloud/rackspace/lb/v1/monitors" + "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes" + "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions" + "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle" + "github.com/rackspace/gophercloud/rackspace/lb/v1/vips" +) + +var ( + errNameRequired = errors.New("Name is a required attribute") + errTimeoutExceeded = errors.New("Timeout must be less than 120") +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToLBListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. +type ListOpts struct { + ChangesSince string `q:"changes-since"` + Status Status `q:"status"` + NodeAddr string `q:"nodeaddress"` + Marker string `q:"marker"` + Limit int `q:"limit"` +} + +// ToLBListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLBListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List is the operation responsible for returning a paginated collection of +// load balancers. You may pass in a ListOpts struct to filter results. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(client) + if opts != nil { + query, err := opts.ToLBListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return LBPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToLBCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Required - name of the load balancer to create. The name must be 128 + // characters or fewer in length, and all UTF-8 characters are valid. + Name string + + // Optional - nodes to be added. + Nodes []nodes.Node + + // Required - protocol of the service that is being load balanced. + // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html + // for a full list of supported protocols. + Protocol string + + // Optional - enables or disables Half-Closed support for the load balancer. + // Half-Closed support provides the ability for one end of the connection to + // terminate its output, while still receiving data from the other end. Only + // available for TCP/TCP_CLIENT_FIRST protocols. + HalfClosed gophercloud.EnabledState + + // Optional - the type of virtual IPs you want associated with the load + // balancer. + VIPs []vips.VIP + + // Optional - the access list management feature allows fine-grained network + // access controls to be applied to the load balancer virtual IP address. + AccessList *acl.AccessList + + // Optional - algorithm that defines how traffic should be directed between + // back-end nodes. + Algorithm string + + // Optional - current connection logging configuration. + ConnectionLogging *ConnectionLogging + + // Optional - specifies a limit on the number of connections per IP address + // to help mitigate malicious or abusive traffic to your applications. + ConnThrottle *throttle.ConnectionThrottle + + // Optional - the type of health monitor check to perform to ensure that the + // service is performing properly. + HealthMonitor *monitors.Monitor + + // Optional - arbitrary information that can be associated with each LB. + Metadata map[string]interface{} + + // Optional - port number for the service you are load balancing. + Port int + + // Optional - the timeout value for the load balancer and communications with + // its nodes. Defaults to 30 seconds with a maximum of 120 seconds. + Timeout int + + // Optional - specifies whether multiple requests from clients are directed + // to the same node. + SessionPersistence *sessions.SessionPersistence + + // Optional - enables or disables HTTP to HTTPS redirection for the load + // balancer. When enabled, any HTTP request returns status code 301 (Moved + // Permanently), and the requester is redirected to the requested URL via the + // HTTPS protocol on port 443. For example, http://example.com/page.html + // would be redirected to https://example.com/page.html. Only available for + // HTTPS protocol (port=443), or HTTP protocol with a properly configured SSL + // termination (secureTrafficOnly=true, securePort=443). + HTTPSRedirect gophercloud.EnabledState +} + +// ToLBCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToLBCreateMap() (map[string]interface{}, error) { + lb := make(map[string]interface{}) + + if opts.Name == "" { + return lb, errNameRequired + } + if opts.Timeout > 120 { + return lb, errTimeoutExceeded + } + + lb["name"] = opts.Name + + if len(opts.Nodes) > 0 { + nodes := []map[string]interface{}{} + for _, n := range opts.Nodes { + nodes = append(nodes, map[string]interface{}{ + "address": n.Address, + "port": n.Port, + "condition": n.Condition, + }) + } + lb["nodes"] = nodes + } + + if opts.Protocol != "" { + lb["protocol"] = opts.Protocol + } + if opts.HalfClosed != nil { + lb["halfClosed"] = opts.HalfClosed + } + if len(opts.VIPs) > 0 { + lb["virtualIps"] = opts.VIPs + } + if opts.AccessList != nil { + lb["accessList"] = &opts.AccessList + } + if opts.Algorithm != "" { + lb["algorithm"] = opts.Algorithm + } + if opts.ConnectionLogging != nil { + lb["connectionLogging"] = &opts.ConnectionLogging + } + if opts.ConnThrottle != nil { + lb["connectionThrottle"] = &opts.ConnThrottle + } + if opts.HealthMonitor != nil { + lb["healthMonitor"] = &opts.HealthMonitor + } + if len(opts.Metadata) != 0 { + lb["metadata"] = opts.Metadata + } + if opts.Port > 0 { + lb["port"] = opts.Port + } + if opts.Timeout > 0 { + lb["timeout"] = opts.Timeout + } + if opts.SessionPersistence != nil { + lb["sessionPersistence"] = &opts.SessionPersistence + } + if opts.HTTPSRedirect != nil { + lb["httpsRedirect"] = &opts.HTTPSRedirect + } + + return map[string]interface{}{"loadBalancer": lb}, nil +} + +// Create is the operation responsible for asynchronously provisioning a new +// load balancer based on the configuration defined in CreateOpts. Once the +// request is validated and progress has started on the provisioning process, a +// response struct is returned. When extracted (with Extract()), you have +// to the load balancer's unique ID and status. +// +// Once an ID is attained, you can check on the progress of the operation by +// calling Get and passing in the ID. If the corresponding request cannot be +// fulfilled due to insufficient or invalid data, an HTTP 400 (Bad Request) +// error response is returned with information regarding the nature of the +// failure in the body of the response. Failures in the validation process are +// non-recoverable and require the caller to correct the cause of the failure. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToLBCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + + return res +} + +// Get is the operation responsible for providing detailed information +// regarding a specific load balancer which is configured and associated with +// your account. This operation is not capable of returning details for a load +// balancer which has been deleted. +func Get(c *gophercloud.ServiceClient, id int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// BulkDelete removes all the load balancers referenced in the slice of IDs. +// Any and all configuration data associated with these load balancers is +// immediately purged and is not recoverable. +// +// If one of the items in the list cannot be removed due to its current status, +// a 400 Bad Request error is returned along with the IDs of the ones the +// system identified as potential failures for this request. +func BulkDelete(c *gophercloud.ServiceClient, ids []int) DeleteResult { + var res DeleteResult + + if len(ids) > 10 || len(ids) == 0 { + res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 LB IDs") + return res + } + + url := rootURL(c) + url += gophercloud.IDSliceToQueryString("id", ids) + + _, res.Err = perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// Delete removes a single load balancer. +func Delete(c *gophercloud.ServiceClient, id int) DeleteResult { + var res DeleteResult + + _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// UpdateOptsBuilder represents a type that can be converted into a JSON-like +// map structure. +type UpdateOptsBuilder interface { + ToLBUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the options for updating an existing load balancer. +type UpdateOpts struct { + // Optional - new name of the load balancer. + Name string + + // Optional - the new protocol you want your load balancer to have. + // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html + // for a full list of supported protocols. + Protocol string + + // Optional - see the HalfClosed field in CreateOpts for more information. + HalfClosed gophercloud.EnabledState + + // Optional - see the Algorithm field in CreateOpts for more information. + Algorithm string + + // Optional - see the Port field in CreateOpts for more information. + Port int + + // Optional - see the Timeout field in CreateOpts for more information. + Timeout int + + // Optional - see the HTTPSRedirect field in CreateOpts for more information. + HTTPSRedirect gophercloud.EnabledState +} + +// ToLBUpdateMap casts an UpdateOpts struct to a map. +func (opts UpdateOpts) ToLBUpdateMap() (map[string]interface{}, error) { + lb := make(map[string]interface{}) + + if opts.Name != "" { + lb["name"] = opts.Name + } + if opts.Protocol != "" { + lb["protocol"] = opts.Protocol + } + if opts.HalfClosed != nil { + lb["halfClosed"] = opts.HalfClosed + } + if opts.Algorithm != "" { + lb["algorithm"] = opts.Algorithm + } + if opts.Port > 0 { + lb["port"] = opts.Port + } + if opts.Timeout > 0 { + lb["timeout"] = opts.Timeout + } + if opts.HTTPSRedirect != nil { + lb["httpsRedirect"] = &opts.HTTPSRedirect + } + + return map[string]interface{}{"loadBalancer": lb}, nil +} + +// Update is the operation responsible for asynchronously updating the +// attributes of a specific load balancer. Upon successful validation of the +// request, the service returns a 202 Accepted response, and the load balancer +// enters a PENDING_UPDATE state. A user can poll the load balancer with Get to +// wait for the changes to be applied. When this happens, the load balancer will +// return to an ACTIVE state. +func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToLBUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// ListProtocols is the operation responsible for returning a paginated +// collection of load balancer protocols. +func ListProtocols(client *gophercloud.ServiceClient) pagination.Pager { + url := protocolsURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ProtocolPage{pagination.SinglePageBase(r)} + }) +} + +// ListAlgorithms is the operation responsible for returning a paginated +// collection of load balancer algorithms. +func ListAlgorithms(client *gophercloud.ServiceClient) pagination.Pager { + url := algorithmsURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AlgorithmPage{pagination.SinglePageBase(r)} + }) +} + +// IsLoggingEnabled returns true if the load balancer has connection logging +// enabled and false if not. +func IsLoggingEnabled(client *gophercloud.ServiceClient, id int) (bool, error) { + var body interface{} + + _, err := perigee.Request("GET", loggingURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &body, + OkCodes: []int{200}, + }) + if err != nil { + return false, err + } + + var resp struct { + CL struct { + Enabled bool `mapstructure:"enabled"` + } `mapstructure:"connectionLogging"` + } + + err = mapstructure.Decode(body, &resp) + return resp.CL.Enabled, err +} + +func toConnLoggingMap(state bool) map[string]map[string]bool { + return map[string]map[string]bool{ + "connectionLogging": map[string]bool{"enabled": state}, + } +} + +// EnableLogging will enable connection logging for a specified load balancer. +func EnableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + reqBody := toConnLoggingMap(true) + var res gophercloud.ErrResult + + _, res.Err = perigee.Request("PUT", loggingURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// DisableLogging will disable connection logging for a specified load balancer. +func DisableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + reqBody := toConnLoggingMap(false) + var res gophercloud.ErrResult + + _, res.Err = perigee.Request("PUT", loggingURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// GetErrorPage will retrieve the current error page for the load balancer. +func GetErrorPage(client *gophercloud.ServiceClient, id int) ErrorPageResult { + var res ErrorPageResult + + _, res.Err = perigee.Request("GET", errorPageURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// SetErrorPage will set the HTML of the load balancer's error page to a +// specific value. +func SetErrorPage(client *gophercloud.ServiceClient, id int, html string) ErrorPageResult { + var res ErrorPageResult + + type stringMap map[string]string + reqBody := map[string]stringMap{"errorpage": stringMap{"content": html}} + + _, res.Err = perigee.Request("PUT", errorPageURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &res.Body, + ReqBody: &reqBody, + OkCodes: []int{200}, + }) + + return res +} + +// DeleteErrorPage will delete the current error page for the load balancer. +func DeleteErrorPage(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + var res gophercloud.ErrResult + + _, res.Err = perigee.Request("DELETE", errorPageURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return res +} + +// GetStats will retrieve detailed stats related to the load balancer's usage. +func GetStats(client *gophercloud.ServiceClient, id int) StatsResult { + var res StatsResult + + _, res.Err = perigee.Request("GET", statsURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// IsContentCached will check to see whether the specified load balancer caches +// content. When content caching is enabled, recently-accessed files are stored +// on the load balancer for easy retrieval by web clients. Content caching +// improves the performance of high traffic web sites by temporarily storing +// data that was recently accessed. While it's cached, requests for that data +// are served by the load balancer, which in turn reduces load off the back-end +// nodes. The result is improved response times for those requests and less +// load on the web server. +func IsContentCached(client *gophercloud.ServiceClient, id int) (bool, error) { + var body interface{} + + _, err := perigee.Request("GET", cacheURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &body, + OkCodes: []int{200}, + }) + if err != nil { + return false, err + } + + var resp struct { + CC struct { + Enabled bool `mapstructure:"enabled"` + } `mapstructure:"contentCaching"` + } + + err = mapstructure.Decode(body, &resp) + return resp.CC.Enabled, err +} + +func toCachingMap(state bool) map[string]map[string]bool { + return map[string]map[string]bool{ + "contentCaching": map[string]bool{"enabled": state}, + } +} + +// EnableCaching will enable content-caching for the specified load balancer. +func EnableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + reqBody := toCachingMap(true) + var res gophercloud.ErrResult + + _, res.Err = perigee.Request("PUT", cacheURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// DisableCaching will disable content-caching for the specified load balancer. +func DisableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + reqBody := toCachingMap(false) + var res gophercloud.ErrResult + + _, res.Err = perigee.Request("PUT", cacheURL(client, id), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests_test.go new file mode 100644 index 00000000000..a8ec19e07c3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests_test.go @@ -0,0 +1,438 @@ +package lbs + +import ( + "testing" + "time" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes" + "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions" + "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle" + "github.com/rackspace/gophercloud/rackspace/lb/v1/vips" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + id1 = 12345 + id2 = 67890 + ts1 = "2010-11-30T03:23:42Z" + ts2 = "2010-11-30T03:23:44Z" +) + +func toTime(t *testing.T, str string) time.Time { + ts, err := time.Parse(time.RFC3339, str) + if err != nil { + t.Fatalf("Could not parse time: %s", err.Error()) + } + return ts +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListLBResponse(t) + + count := 0 + + err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractLBs(page) + th.AssertNoErr(t, err) + + expected := []LoadBalancer{ + LoadBalancer{ + Name: "lb-site1", + ID: 71, + Protocol: "HTTP", + Port: 80, + Algorithm: "RANDOM", + Status: ACTIVE, + NodeCount: 3, + VIPs: []vips.VIP{ + vips.VIP{ + ID: 403, + Address: "206.55.130.1", + Type: "PUBLIC", + Version: "IPV4", + }, + }, + Created: Datetime{Time: toTime(t, ts1)}, + Updated: Datetime{Time: toTime(t, ts2)}, + }, + LoadBalancer{ + ID: 72, + Name: "lb-site2", + Created: Datetime{Time: toTime(t, "2011-11-30T03:23:42Z")}, + Updated: Datetime{Time: toTime(t, "2011-11-30T03:23:44Z")}, + }, + LoadBalancer{ + ID: 73, + Name: "lb-site3", + Created: Datetime{Time: toTime(t, "2012-11-30T03:23:42Z")}, + Updated: Datetime{Time: toTime(t, "2012-11-30T03:23:44Z")}, + }, + } + + 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() + + mockCreateLBResponse(t) + + opts := CreateOpts{ + Name: "a-new-loadbalancer", + Port: 80, + Protocol: "HTTP", + VIPs: []vips.VIP{ + vips.VIP{ID: 2341}, + vips.VIP{ID: 900001}, + }, + Nodes: []nodes.Node{ + nodes.Node{Address: "10.1.1.1", Port: 80, Condition: "ENABLED"}, + }, + } + + lb, err := Create(client.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := &LoadBalancer{ + Name: "a-new-loadbalancer", + ID: 144, + Protocol: "HTTP", + HalfClosed: false, + Port: 83, + Algorithm: "RANDOM", + Status: BUILD, + Timeout: 30, + Cluster: Cluster{Name: "ztm-n01.staging1.lbaas.rackspace.net"}, + Nodes: []nodes.Node{ + nodes.Node{ + Address: "10.1.1.1", + ID: 653, + Port: 80, + Status: "ONLINE", + Condition: "ENABLED", + Weight: 1, + }, + }, + VIPs: []vips.VIP{ + vips.VIP{ + ID: 39, + Address: "206.10.10.210", + Type: vips.PUBLIC, + Version: vips.IPV4, + }, + vips.VIP{ + ID: 900001, + Address: "2001:4801:79f1:0002:711b:be4c:0000:0021", + Type: vips.PUBLIC, + Version: vips.IPV6, + }, + }, + Created: Datetime{Time: toTime(t, ts1)}, + Updated: Datetime{Time: toTime(t, ts2)}, + ConnectionLogging: ConnectionLogging{Enabled: false}, + } + + th.AssertDeepEquals(t, expected, lb) +} + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + ids := []int{id1, id2} + + mockBatchDeleteLBResponse(t, ids) + + err := BulkDelete(client.ServiceClient(), ids).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteLBResponse(t, id1) + + err := Delete(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetLBResponse(t, id1) + + lb, err := Get(client.ServiceClient(), id1).Extract() + + expected := &LoadBalancer{ + Name: "sample-loadbalancer", + ID: 2000, + Protocol: "HTTP", + Port: 80, + Algorithm: "RANDOM", + Status: ACTIVE, + Timeout: 30, + ConnectionLogging: ConnectionLogging{Enabled: true}, + VIPs: []vips.VIP{ + vips.VIP{ + ID: 1000, + Address: "206.10.10.210", + Type: "PUBLIC", + Version: "IPV4", + }, + }, + Nodes: []nodes.Node{ + nodes.Node{ + Address: "10.1.1.1", + ID: 1041, + Port: 80, + Status: "ONLINE", + Condition: "ENABLED", + }, + nodes.Node{ + Address: "10.1.1.2", + ID: 1411, + Port: 80, + Status: "ONLINE", + Condition: "ENABLED", + }, + }, + SessionPersistence: sessions.SessionPersistence{Type: "HTTP_COOKIE"}, + ConnectionThrottle: throttle.ConnectionThrottle{MaxConnections: 100}, + Cluster: Cluster{Name: "c1.dfw1"}, + Created: Datetime{Time: toTime(t, ts1)}, + Updated: Datetime{Time: toTime(t, ts2)}, + SourceAddrs: SourceAddrs{ + IPv4Public: "10.12.99.28", + IPv4Private: "10.0.0.0", + IPv6Public: "2001:4801:79f1:1::1/64", + }, + } + + th.AssertDeepEquals(t, expected, lb) + th.AssertNoErr(t, err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateLBResponse(t, id1) + + opts := UpdateOpts{ + Name: "a-new-loadbalancer", + Protocol: "TCP", + HalfClosed: gophercloud.Enabled, + Algorithm: "RANDOM", + Port: 8080, + Timeout: 100, + HTTPSRedirect: gophercloud.Disabled, + } + + err := Update(client.ServiceClient(), id1, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestListProtocols(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListProtocolsResponse(t) + + count := 0 + + err := ListProtocols(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractProtocols(page) + th.AssertNoErr(t, err) + + expected := []Protocol{ + Protocol{Name: "DNS_TCP", Port: 53}, + Protocol{Name: "DNS_UDP", Port: 53}, + Protocol{Name: "FTP", Port: 21}, + Protocol{Name: "HTTP", Port: 80}, + Protocol{Name: "HTTPS", Port: 443}, + Protocol{Name: "IMAPS", Port: 993}, + Protocol{Name: "IMAPv4", Port: 143}, + } + + th.CheckDeepEquals(t, expected[0:7], actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestListAlgorithms(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListAlgorithmsResponse(t) + + count := 0 + + err := ListAlgorithms(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractAlgorithms(page) + th.AssertNoErr(t, err) + + expected := []Algorithm{ + Algorithm{Name: "LEAST_CONNECTIONS"}, + Algorithm{Name: "RANDOM"}, + Algorithm{Name: "ROUND_ROBIN"}, + Algorithm{Name: "WEIGHTED_LEAST_CONNECTIONS"}, + Algorithm{Name: "WEIGHTED_ROUND_ROBIN"}, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestIsLoggingEnabled(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetLoggingResponse(t, id1) + + res, err := IsLoggingEnabled(client.ServiceClient(), id1) + th.AssertNoErr(t, err) + th.AssertEquals(t, true, res) +} + +func TestEnablingLogging(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockEnableLoggingResponse(t, id1) + + err := EnableLogging(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDisablingLogging(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDisableLoggingResponse(t, id1) + + err := DisableLogging(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGetErrorPage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetErrorPageResponse(t, id1) + + content, err := GetErrorPage(client.ServiceClient(), id1).Extract() + th.AssertNoErr(t, err) + + expected := &ErrorPage{Content: "DEFAULT ERROR PAGE"} + th.AssertDeepEquals(t, expected, content) +} + +func TestSetErrorPage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockSetErrorPageResponse(t, id1) + + html := "New error page" + content, err := SetErrorPage(client.ServiceClient(), id1, html).Extract() + th.AssertNoErr(t, err) + + expected := &ErrorPage{Content: html} + th.AssertDeepEquals(t, expected, content) +} + +func TestDeleteErrorPage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteErrorPageResponse(t, id1) + + err := DeleteErrorPage(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGetStats(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetStatsResponse(t, id1) + + content, err := GetStats(client.ServiceClient(), id1).Extract() + th.AssertNoErr(t, err) + + expected := &Stats{ + ConnectTimeout: 10, + ConnectError: 20, + ConnectFailure: 30, + DataTimedOut: 40, + KeepAliveTimedOut: 50, + MaxConnections: 60, + CurrentConnections: 40, + SSLConnectTimeout: 10, + SSLConnectError: 20, + SSLConnectFailure: 30, + SSLDataTimedOut: 40, + SSLKeepAliveTimedOut: 50, + SSLMaxConnections: 60, + SSLCurrentConnections: 40, + } + th.AssertDeepEquals(t, expected, content) +} + +func TestIsCached(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetCachingResponse(t, id1) + + res, err := IsContentCached(client.ServiceClient(), id1) + th.AssertNoErr(t, err) + th.AssertEquals(t, true, res) +} + +func TestEnablingCaching(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockEnableCachingResponse(t, id1) + + err := EnableCaching(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDisablingCaching(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDisableCachingResponse(t, id1) + + err := DisableCaching(client.ServiceClient(), id1).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/results.go new file mode 100644 index 00000000000..98f3962d77e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/results.go @@ -0,0 +1,420 @@ +package lbs + +import ( + "reflect" + "time" + + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/lb/v1/acl" + "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes" + "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions" + "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle" + "github.com/rackspace/gophercloud/rackspace/lb/v1/vips" +) + +// Protocol represents the network protocol which the load balancer accepts. +type Protocol struct { + // The name of the protocol, e.g. HTTP, LDAP, FTP, etc. + Name string + + // The port number for the protocol. + Port int +} + +// Algorithm defines how traffic should be directed between back-end nodes. +type Algorithm struct { + // The name of the algorithm, e.g RANDOM, ROUND_ROBIN, etc. + Name string +} + +// Status represents the potential state of a load balancer resource. +type Status string + +const ( + // ACTIVE indicates that the LB is configured properly and ready to serve + // traffic to incoming requests via the configured virtual IPs. + ACTIVE Status = "ACTIVE" + + // BUILD indicates that the LB is being provisioned for the first time and + // configuration is being applied to bring the service online. The service + // cannot yet serve incoming requests. + BUILD Status = "BUILD" + + // PENDINGUPDATE indicates that the LB is online but configuration changes + // are being applied to update the service based on a previous request. + PENDINGUPDATE Status = "PENDING_UPDATE" + + // PENDINGDELETE indicates that the LB is online but configuration changes + // are being applied to begin deletion of the service based on a previous + // request. + PENDINGDELETE Status = "PENDING_DELETE" + + // SUSPENDED indicates that the LB has been taken offline and disabled. + SUSPENDED Status = "SUSPENDED" + + // ERROR indicates that the system encountered an error when attempting to + // configure the load balancer. + ERROR Status = "ERROR" + + // DELETED indicates that the LB has been deleted. + DELETED Status = "DELETED" +) + +// Datetime represents the structure of a Created or Updated field. +type Datetime struct { + Time time.Time `mapstructure:"-"` +} + +// LoadBalancer represents a load balancer API resource. +type LoadBalancer struct { + // Human-readable name for the load balancer. + Name string + + // The unique ID for the load balancer. + ID int + + // Represents the service protocol being load balanced. See Protocol type for + // a list of accepted values. + // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html + // for a full list of supported protocols. + Protocol string + + // Defines how traffic should be directed between back-end nodes. The default + // algorithm is RANDOM. See Algorithm type for a list of accepted values. + Algorithm string + + // The current status of the load balancer. + Status Status + + // The number of load balancer nodes. + NodeCount int `mapstructure:"nodeCount"` + + // Slice of virtual IPs associated with this load balancer. + VIPs []vips.VIP `mapstructure:"virtualIps"` + + // Datetime when the LB was created. + Created Datetime + + // Datetime when the LB was created. + Updated Datetime + + // Port number for the service you are load balancing. + Port int + + // HalfClosed provides the ability for one end of the connection to + // terminate its output while still receiving data from the other end. This + // is only available on TCP/TCP_CLIENT_FIRST protocols. + HalfClosed bool + + // Timeout represents the timeout value between a load balancer and its + // nodes. Defaults to 30 seconds with a maximum of 120 seconds. + Timeout int + + // The cluster name. + Cluster Cluster + + // Nodes shows all the back-end nodes which are associated with the load + // balancer. These are the devices which are delivered traffic. + Nodes []nodes.Node + + // Current connection logging configuration. + ConnectionLogging ConnectionLogging + + // SessionPersistence specifies whether multiple requests from clients are + // directed to the same node. + SessionPersistence sessions.SessionPersistence + + // ConnectionThrottle specifies a limit on the number of connections per IP + // address to help mitigate malicious or abusive traffic to your applications. + ConnectionThrottle throttle.ConnectionThrottle + + // The source public and private IP addresses. + SourceAddrs SourceAddrs `mapstructure:"sourceAddresses"` + + // Represents the access rules for this particular load balancer. IP addresses + // or subnet ranges, depending on their type (ALLOW or DENY), can be permitted + // or blocked. + AccessList acl.AccessList +} + +// SourceAddrs represents the source public and private IP addresses. +type SourceAddrs struct { + IPv4Public string `json:"ipv4Public" mapstructure:"ipv4Public"` + IPv4Private string `json:"ipv4Servicenet" mapstructure:"ipv4Servicenet"` + IPv6Public string `json:"ipv6Public" mapstructure:"ipv6Public"` + IPv6Private string `json:"ipv6Servicenet" mapstructure:"ipv6Servicenet"` +} + +// ConnectionLogging - temp +type ConnectionLogging struct { + Enabled bool +} + +// Cluster - temp +type Cluster struct { + Name string +} + +// LBPage is the page returned by a pager when traversing over a collection of +// LBs. +type LBPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (p LBPage) IsEmpty() (bool, error) { + is, err := ExtractLBs(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractLBs accepts a Page struct, specifically a LBPage struct, and extracts +// the elements into a slice of LoadBalancer structs. In other words, a generic +// collection is mapped into a relevant slice. +func ExtractLBs(page pagination.Page) ([]LoadBalancer, error) { + var resp struct { + LBs []LoadBalancer `mapstructure:"loadBalancers" json:"loadBalancers"` + } + + coll := page.(LBPage).Body + err := mapstructure.Decode(coll, &resp) + + s := reflect.ValueOf(coll.(map[string]interface{})["loadBalancers"]) + + for i := 0; i < s.Len(); i++ { + val := (s.Index(i).Interface()).(map[string]interface{}) + + ts, err := extractTS(val, "created") + if err != nil { + return resp.LBs, err + } + resp.LBs[i].Created.Time = ts + + ts, err = extractTS(val, "updated") + if err != nil { + return resp.LBs, err + } + resp.LBs[i].Updated.Time = ts + } + + return resp.LBs, err +} + +func extractTS(body map[string]interface{}, key string) (time.Time, error) { + val := body[key].(map[string]interface{}) + return time.Parse(time.RFC3339, val["time"].(string)) +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as a LB, if possible. +func (r commonResult) Extract() (*LoadBalancer, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + LB LoadBalancer `mapstructure:"loadBalancer"` + } + + err := mapstructure.Decode(r.Body, &response) + + json := r.Body.(map[string]interface{}) + lb := json["loadBalancer"].(map[string]interface{}) + + ts, err := extractTS(lb, "created") + if err != nil { + return nil, err + } + response.LB.Created.Time = ts + + ts, err = extractTS(lb, "updated") + if err != nil { + return nil, err + } + response.LB.Updated.Time = ts + + return &response.LB, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// ProtocolPage is the page returned by a pager when traversing over a +// collection of LB protocols. +type ProtocolPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether a ProtocolPage struct is empty. +func (p ProtocolPage) IsEmpty() (bool, error) { + is, err := ExtractProtocols(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractProtocols accepts a Page struct, specifically a ProtocolPage struct, +// and extracts the elements into a slice of Protocol structs. In other +// words, a generic collection is mapped into a relevant slice. +func ExtractProtocols(page pagination.Page) ([]Protocol, error) { + var resp struct { + Protocols []Protocol `mapstructure:"protocols" json:"protocols"` + } + err := mapstructure.Decode(page.(ProtocolPage).Body, &resp) + return resp.Protocols, err +} + +// AlgorithmPage is the page returned by a pager when traversing over a +// collection of LB algorithms. +type AlgorithmPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an AlgorithmPage struct is empty. +func (p AlgorithmPage) IsEmpty() (bool, error) { + is, err := ExtractAlgorithms(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractAlgorithms accepts a Page struct, specifically an AlgorithmPage struct, +// and extracts the elements into a slice of Algorithm structs. In other +// words, a generic collection is mapped into a relevant slice. +func ExtractAlgorithms(page pagination.Page) ([]Algorithm, error) { + var resp struct { + Algorithms []Algorithm `mapstructure:"algorithms" json:"algorithms"` + } + err := mapstructure.Decode(page.(AlgorithmPage).Body, &resp) + return resp.Algorithms, err +} + +// ErrorPage represents the HTML file that is shown to an end user who is +// attempting to access a load balancer node that is offline/unavailable. +// +// During provisioning, every load balancer is configured with a default error +// page that gets displayed when traffic is requested for an offline node. +// +// You can add a single custom error page with an HTTP-based protocol to a load +// balancer. Page updates override existing content. If a custom error page is +// deleted, or the load balancer is changed to a non-HTTP protocol, the default +// error page is restored. +type ErrorPage struct { + Content string +} + +// ErrorPageResult represents the result of an error page operation - +// specifically getting or creating one. +type ErrorPageResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as an ErrorPage, if possible. +func (r ErrorPageResult) Extract() (*ErrorPage, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + ErrorPage ErrorPage `mapstructure:"errorpage"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.ErrorPage, err +} + +// Stats represents all the key information about a load balancer's usage. +type Stats struct { + // The number of connections closed by this load balancer because its + // ConnectTimeout interval was exceeded. + ConnectTimeout int `mapstructure:"connectTimeOut"` + + // The number of transaction or protocol errors for this load balancer. + ConnectError int + + // Number of connection failures for this load balancer. + ConnectFailure int + + // Number of connections closed by this load balancer because its Timeout + // interval was exceeded. + DataTimedOut int + + // Number of connections closed by this load balancer because the + // 'keepalive_timeout' interval was exceeded. + KeepAliveTimedOut int + + // The maximum number of simultaneous TCP connections this load balancer has + // processed at any one time. + MaxConnections int `mapstructure:"maxConn"` + + // Number of simultaneous connections active at the time of the request. + CurrentConnections int `mapstructure:"currentConn"` + + // Number of SSL connections closed by this load balancer because the + // ConnectTimeout interval was exceeded. + SSLConnectTimeout int `mapstructure:"connectTimeOutSsl"` + + // Number of SSL transaction or protocol erros in this load balancer. + SSLConnectError int `mapstructure:"connectErrorSsl"` + + // Number of SSL connection failures in this load balancer. + SSLConnectFailure int `mapstructure:"connectFailureSsl"` + + // Number of SSL connections closed by this load balancer because the + // Timeout interval was exceeded. + SSLDataTimedOut int `mapstructure:"dataTimedOutSsl"` + + // Number of SSL connections closed by this load balancer because the + // 'keepalive_timeout' interval was exceeded. + SSLKeepAliveTimedOut int `mapstructure:"keepAliveTimedOutSsl"` + + // Maximum number of simultaneous SSL connections this load balancer has + // processed at any one time. + SSLMaxConnections int `mapstructure:"maxConnSsl"` + + // Number of simultaneous SSL connections active at the time of the request. + SSLCurrentConnections int `mapstructure:"currentConnSsl"` +} + +// StatsResult represents the result of a Stats operation. +type StatsResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as a Stats struct, if possible. +func (r StatsResult) Extract() (*Stats, error) { + if r.Err != nil { + return nil, r.Err + } + res := &Stats{} + err := mapstructure.Decode(r.Body, res) + return res, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/urls.go new file mode 100644 index 00000000000..471a86b0a7e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/urls.go @@ -0,0 +1,49 @@ +package lbs + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + protocolsPath = "protocols" + algorithmsPath = "algorithms" + logPath = "connectionlogging" + epPath = "errorpage" + stPath = "stats" + cachePath = "contentcaching" +) + +func resourceURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id)) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(path) +} + +func protocolsURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(path, protocolsPath) +} + +func algorithmsURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(path, algorithmsPath) +} + +func loggingURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), logPath) +} + +func errorPageURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), epPath) +} + +func statsURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), stPath) +} + +func cacheURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), cachePath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/doc.go new file mode 100644 index 00000000000..2c5be75ae42 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/doc.go @@ -0,0 +1,21 @@ +/* +Package monitors provides information and interaction with the Health Monitor +API resource for the Rackspace Cloud Load Balancer service. + +The load balancing service includes a health monitoring resource that +periodically checks your back-end nodes to ensure they are responding correctly. +If a node does not respond, it is removed from rotation until the health monitor +determines that the node is functional. In addition to being performed +periodically, a health check also executes against every new node that is +added, to ensure that the node is operating properly before allowing it to +service traffic. Only one health monitor is allowed to be enabled on a load +balancer at a time. + +As part of a good strategy for monitoring connections, secondary nodes should +also be created which provide failover for effectively routing traffic in case +the primary node fails. This is an additional feature that ensures that you +remain up in case your primary node fails. + +There are three types of health monitor: CONNECT, HTTP and HTTPS. +*/ +package monitors diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/fixtures.go new file mode 100644 index 00000000000..a565abced54 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/fixtures.go @@ -0,0 +1,87 @@ +package monitors + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(lbID int) string { + return "/loadbalancers/" + strconv.Itoa(lbID) + "/healthmonitor" +} + +func mockGetResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "healthMonitor": { + "type": "CONNECT", + "delay": 10, + "timeout": 10, + "attemptsBeforeDeactivation": 3 + } +} + `) + }) +} + +func mockUpdateConnectResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "healthMonitor": { + "type": "CONNECT", + "delay": 10, + "timeout": 10, + "attemptsBeforeDeactivation": 3 + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockUpdateHTTPResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "healthMonitor": { + "attemptsBeforeDeactivation": 3, + "bodyRegex": "{regex}", + "delay": 10, + "path": "/foo", + "statusRegex": "200", + "timeout": 10, + "type": "HTTPS" + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests.go new file mode 100644 index 00000000000..cfc35d2ef75 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests.go @@ -0,0 +1,178 @@ +package monitors + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" +) + +var ( + errAttemptLimit = errors.New("AttemptLimit field must be an int greater than 1 and less than 10") + errDelay = errors.New("Delay field must be an int greater than 1 and less than 10") + errTimeout = errors.New("Timeout field must be an int greater than 1 and less than 10") +) + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. +type UpdateOptsBuilder interface { + ToMonitorUpdateMap() (map[string]interface{}, error) +} + +// UpdateConnectMonitorOpts represents the options needed to update a CONNECT +// monitor. +type UpdateConnectMonitorOpts struct { + // Required - number of permissible monitor failures before removing a node + // from rotation. Must be a number between 1 and 10. + AttemptLimit int + + // Required - the minimum number of seconds to wait before executing the + // health monitor. Must be a number between 1 and 3600. + Delay int + + // Required - maximum number of seconds to wait for a connection to be + // established before timing out. Must be a number between 1 and 300. + Timeout int +} + +// ToMonitorUpdateMap produces a map for updating CONNECT monitors. +func (opts UpdateConnectMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) { + type m map[string]interface{} + + if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) { + return m{}, errAttemptLimit + } + if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) { + return m{}, errDelay + } + if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) { + return m{}, errTimeout + } + + return m{"healthMonitor": m{ + "attemptsBeforeDeactivation": opts.AttemptLimit, + "delay": opts.Delay, + "timeout": opts.Timeout, + "type": CONNECT, + }}, nil +} + +// UpdateHTTPMonitorOpts represents the options needed to update a HTTP monitor. +type UpdateHTTPMonitorOpts struct { + // Required - number of permissible monitor failures before removing a node + // from rotation. Must be a number between 1 and 10. + AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"` + + // Required - the minimum number of seconds to wait before executing the + // health monitor. Must be a number between 1 and 3600. + Delay int + + // Required - maximum number of seconds to wait for a connection to be + // established before timing out. Must be a number between 1 and 300. + Timeout int + + // Required - a regular expression that will be used to evaluate the contents + // of the body of the response. + BodyRegex string + + // Required - the HTTP path that will be used in the sample request. + Path string + + // Required - a regular expression that will be used to evaluate the HTTP + // status code returned in the response. + StatusRegex string + + // Optional - the name of a host for which the health monitors will check. + HostHeader string + + // Required - either HTTP or HTTPS + Type Type +} + +// ToMonitorUpdateMap produces a map for updating HTTP(S) monitors. +func (opts UpdateHTTPMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) { + type m map[string]interface{} + + if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) { + return m{}, errAttemptLimit + } + if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) { + return m{}, errDelay + } + if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) { + return m{}, errTimeout + } + if opts.Type != HTTP && opts.Type != HTTPS { + return m{}, errors.New("Type must either by HTTP or HTTPS") + } + if opts.BodyRegex == "" { + return m{}, errors.New("BodyRegex is a required field") + } + if opts.Path == "" { + return m{}, errors.New("Path is a required field") + } + if opts.StatusRegex == "" { + return m{}, errors.New("StatusRegex is a required field") + } + + json := m{ + "attemptsBeforeDeactivation": opts.AttemptLimit, + "delay": opts.Delay, + "timeout": opts.Timeout, + "type": opts.Type, + "bodyRegex": opts.BodyRegex, + "path": opts.Path, + "statusRegex": opts.StatusRegex, + } + + if opts.HostHeader != "" { + json["hostHeader"] = opts.HostHeader + } + + return m{"healthMonitor": json}, nil +} + +// Update is the operation responsible for updating a health monitor. +func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToMonitorUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", rootURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// Get is the operation responsible for showing details of a health monitor. +func Get(c *gophercloud.ServiceClient, id int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", rootURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Delete is the operation responsible for deleting a health monitor. +func Delete(c *gophercloud.ServiceClient, id int) DeleteResult { + var res DeleteResult + + _, res.Err = perigee.Request("DELETE", rootURL(c, id), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests_test.go new file mode 100644 index 00000000000..76a60db7f45 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests_test.go @@ -0,0 +1,75 @@ +package monitors + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const lbID = 12345 + +func TestUpdateCONNECT(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateConnectResponse(t, lbID) + + opts := UpdateConnectMonitorOpts{ + AttemptLimit: 3, + Delay: 10, + Timeout: 10, + } + + err := Update(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestUpdateHTTP(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateHTTPResponse(t, lbID) + + opts := UpdateHTTPMonitorOpts{ + AttemptLimit: 3, + Delay: 10, + Timeout: 10, + BodyRegex: "{regex}", + Path: "/foo", + StatusRegex: "200", + Type: HTTPS, + } + + err := Update(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetResponse(t, lbID) + + m, err := Get(client.ServiceClient(), lbID).Extract() + th.AssertNoErr(t, err) + + expected := &Monitor{ + Type: CONNECT, + Delay: 10, + Timeout: 10, + AttemptLimit: 3, + } + + th.AssertDeepEquals(t, expected, m) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID) + + err := Delete(client.ServiceClient(), lbID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/results.go new file mode 100644 index 00000000000..eec556f343c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/results.go @@ -0,0 +1,90 @@ +package monitors + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" +) + +// Type represents the type of Monitor. +type Type string + +// Useful constants. +const ( + CONNECT Type = "CONNECT" + HTTP Type = "HTTP" + HTTPS Type = "HTTPS" +) + +// Monitor represents a health monitor API resource. A monitor comes in three +// forms: CONNECT, HTTP or HTTPS. +// +// A CONNECT monitor establishes a basic connection to each node on its defined +// port to ensure that the service is listening properly. The connect monitor +// is the most basic type of health check and does no post-processing or +// protocol-specific health checks. +// +// HTTP and HTTPS health monitors are generally considered more intelligent and +// powerful than CONNECT. It is capable of processing an HTTP or HTTPS response +// to determine the condition of a node. It supports the same basic properties +// as CONNECT and includes additional attributes that are used to evaluate the +// HTTP response. +type Monitor struct { + // Number of permissible monitor failures before removing a node from + // rotation. + AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"` + + // The minimum number of seconds to wait before executing the health monitor. + Delay int + + // Maximum number of seconds to wait for a connection to be established + // before timing out. + Timeout int + + // Type of the health monitor. + Type Type + + // A regular expression that will be used to evaluate the contents of the + // body of the response. + BodyRegex string + + // The name of a host for which the health monitors will check. + HostHeader string + + // The HTTP path that will be used in the sample request. + Path string + + // A regular expression that will be used to evaluate the HTTP status code + // returned in the response. + StatusRegex string +} + +// UpdateResult represents the result of an Update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// DeleteResult represents the result of an Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Extract interprets any GetResult as a Monitor. +func (r GetResult) Extract() (*Monitor, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + M Monitor `mapstructure:"healthMonitor"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.M, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/urls.go new file mode 100644 index 00000000000..0a1e6df5f49 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/urls.go @@ -0,0 +1,16 @@ +package monitors + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + monitorPath = "healthmonitor" +) + +func rootURL(c *gophercloud.ServiceClient, lbID int) string { + return c.ServiceURL(path, strconv.Itoa(lbID), monitorPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/doc.go new file mode 100644 index 00000000000..49c431894a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/doc.go @@ -0,0 +1,35 @@ +/* +Package nodes provides information and interaction with the Node API resource +for the Rackspace Cloud Load Balancer service. + +Nodes are responsible for servicing the requests received through the load +balancer's virtual IP. A node is usually a virtual machine. By default, the +load balancer employs a basic health check that ensures the node is listening +on its defined port. The node is checked at the time of addition and at regular +intervals as defined by the load balancer's health check configuration. If a +back-end node is not listening on its port, or does not meet the conditions of +the defined check, then connections will not be forwarded to the node, and its +status is changed to OFFLINE. Only nodes that are in an ONLINE status receive +and can service traffic from the load balancer. + +All nodes have an associated status that indicates whether the node is +ONLINE, OFFLINE, or DRAINING. Only nodes that are in ONLINE status can receive +and service traffic from the load balancer. The OFFLINE status represents a +node that cannot accept or service traffic. A node in DRAINING status +represents a node that stops the traffic manager from sending any additional +new connections to the node, but honors established sessions. If the traffic +manager receives a request and session persistence requires that the node is +used, the traffic manager uses it. The status is determined by the passive or +active health monitors. + +If the WEIGHTED_ROUND_ROBIN load balancer algorithm mode is selected, then the +caller should assign the relevant weights to the node as part of the weight +attribute of the node element. When the algorithm of the load balancer is +changed to WEIGHTED_ROUND_ROBIN and the nodes do not already have an assigned +weight, the service automatically sets the weight to 1 for all nodes. + +One or more secondary nodes can be added to a specified load balancer so that +if all the primary nodes fail, traffic can be redirected to secondary nodes. +The type attribute allows configuring the node as either PRIMARY or SECONDARY. +*/ +package nodes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go new file mode 100644 index 00000000000..7c85945cafe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/fixtures.go @@ -0,0 +1,207 @@ +package nodes + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(lbID int) string { + return "/loadbalancers/" + strconv.Itoa(lbID) + "/nodes" +} + +func _nodeURL(lbID, nodeID int) string { + return _rootURL(lbID) + "/" + strconv.Itoa(nodeID) +} + +func mockListResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "nodes": [ + { + "id": 410, + "address": "10.1.1.1", + "port": 80, + "condition": "ENABLED", + "status": "ONLINE", + "weight": 3, + "type": "PRIMARY" + }, + { + "id": 411, + "address": "10.1.1.2", + "port": 80, + "condition": "ENABLED", + "status": "ONLINE", + "weight": 8, + "type": "SECONDARY" + } + ] +} + `) + }) +} + +func mockCreateResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "nodes": [ + { + "address": "10.2.2.3", + "port": 80, + "condition": "ENABLED", + "type": "PRIMARY" + }, + { + "address": "10.2.2.4", + "port": 81, + "condition": "ENABLED", + "type": "SECONDARY" + } + ] +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "nodes": [ + { + "address": "10.2.2.3", + "id": 185, + "port": 80, + "status": "ONLINE", + "condition": "ENABLED", + "weight": 1, + "type": "PRIMARY" + }, + { + "address": "10.2.2.4", + "id": 186, + "port": 81, + "status": "ONLINE", + "condition": "ENABLED", + "weight": 1, + "type": "SECONDARY" + } + ] +} + `) + }) +} + +func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + r.ParseForm() + + for k, v := range ids { + fids := r.Form["id"] + th.AssertEquals(t, strconv.Itoa(v), fids[k]) + } + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteResponse(t *testing.T, lbID, nodeID int) { + th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockGetResponse(t *testing.T, lbID, nodeID int) { + th.Mux.HandleFunc(_nodeURL(lbID, nodeID), 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, ` +{ + "node": { + "id": 410, + "address": "10.1.1.1", + "port": 80, + "condition": "ENABLED", + "status": "ONLINE", + "weight": 12, + "type": "PRIMARY" + } +} + `) + }) +} + +func mockUpdateResponse(t *testing.T, lbID, nodeID int) { + th.Mux.HandleFunc(_nodeURL(lbID, nodeID), 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, ` +{ + "node": { + "condition": "DRAINING", + "weight": 10, + "type": "SECONDARY" + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockListEventsResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID)+"/events", 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, ` +{ + "nodeServiceEvents": [ + { + "detailedMessage": "Node is ok", + "nodeId": 373, + "id": 7, + "type": "UPDATE_NODE", + "description": "Node '373' status changed to 'ONLINE' for load balancer '323'", + "category": "UPDATE", + "severity": "INFO", + "relativeUri": "/406271/loadbalancers/323/nodes/373/events", + "accountId": 406271, + "loadbalancerId": 323, + "title": "Node Status Updated", + "author": "Rackspace Cloud", + "created": "10-30-2012 10:18:23" + } + ] +} +`) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go new file mode 100644 index 00000000000..77894aaa01a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go @@ -0,0 +1,279 @@ +package nodes + +import ( + "errors" + "fmt" + + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List is the operation responsible for returning a paginated collection of +// load balancer nodes. It requires the node ID, its parent load balancer ID, +// and optional limit integer (passed in either as a pointer or a nil poitner). +func List(client *gophercloud.ServiceClient, loadBalancerID int, limit *int) pagination.Pager { + url := rootURL(client, loadBalancerID) + if limit != nil { + url += fmt.Sprintf("?limit=%d", limit) + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return NodePage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder is the interface responsible for generating the JSON +// for a Create operation. +type CreateOptsBuilder interface { + ToNodeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is a slice of CreateOpt structs, that allow the user to create +// multiple nodes in a single operation (one node per CreateOpt). +type CreateOpts []CreateOpt + +// CreateOpt represents the options to create a single node. +type CreateOpt struct { + // Required - the IP address or CIDR for this back-end node. It can either be + // a private IP (ServiceNet) or a public IP. + Address string + + // Optional - the port on which traffic is sent and received. + Port int + + // Optional - the condition of the node. See the consts in Results.go. + Condition Condition + + // Optional - the type of the node. See the consts in Results.go. + Type Type + + // Optional - a pointer to an integer between 0 and 100. + Weight *int +} + +func validateWeight(weight *int) error { + if weight != nil && (*weight > 100 || *weight < 0) { + return errors.New("Weight must be a valid int between 0 and 100") + } + return nil +} + +// ToNodeCreateMap converts a slice of options into a map that can be used for +// the JSON. +func (opts CreateOpts) ToNodeCreateMap() (map[string]interface{}, error) { + type nodeMap map[string]interface{} + nodes := []nodeMap{} + + for k, v := range opts { + if v.Address == "" { + return nodeMap{}, fmt.Errorf("ID is a required attribute, none provided for %d CreateOpt element", k) + } + if weightErr := validateWeight(v.Weight); weightErr != nil { + return nodeMap{}, weightErr + } + + node := make(map[string]interface{}) + node["address"] = v.Address + + if v.Port > 0 { + node["port"] = v.Port + } + if v.Condition != "" { + node["condition"] = v.Condition + } + if v.Type != "" { + node["type"] = v.Type + } + if v.Weight != nil { + node["weight"] = &v.Weight + } + + nodes = append(nodes, node) + } + + return nodeMap{"nodes": nodes}, nil +} + +// Create is the operation responsible for creating a new node on a load +// balancer. Since every load balancer exists in both ServiceNet and the public +// Internet, both private and public IP addresses can be used for nodes. +// +// If nodes need time to boot up services before they become operational, you +// can temporarily prevent traffic from being sent to that node by setting the +// Condition field to DRAINING. Health checks will still be performed; but once +// your node is ready, you can update its condition to ENABLED and have it +// handle traffic. +func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToNodeCreateMap() + if err != nil { + res.Err = err + return res + } + + resp, err := perigee.Request("POST", rootURL(client, loadBalancerID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + if err != nil { + res.Err = err + return res + } + + pr, err := pagination.PageResultFrom(resp.HttpResponse) + if err != nil { + res.Err = err + return res + } + + return CreateResult{pagination.SinglePageBase(pr)} +} + +// BulkDelete is the operation responsible for batch deleting multiple nodes in +// a single operation. It accepts a slice of integer IDs and will remove them +// from the load balancer. The maximum limit is 10 node removals at once. +func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, nodeIDs []int) DeleteResult { + var res DeleteResult + + if len(nodeIDs) > 10 || len(nodeIDs) == 0 { + res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 node IDs") + return res + } + + url := rootURL(c, loadBalancerID) + url += gophercloud.IDSliceToQueryString("id", nodeIDs) + + _, res.Err = perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// Get is the operation responsible for showing details for a single node. +func Get(c *gophercloud.ServiceClient, lbID, nodeID int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", resourceURL(c, lbID, nodeID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// UpdateOptsBuilder represents a type that can be converted into a JSON-like +// map structure. +type UpdateOptsBuilder interface { + ToNodeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represent the options for updating an existing node. +type UpdateOpts struct { + // Optional - the condition of the node. See the consts in Results.go. + Condition Condition + + // Optional - the type of the node. See the consts in Results.go. + Type Type + + // Optional - a pointer to an integer between 0 and 100. + Weight *int +} + +// ToNodeUpdateMap converts an options struct into a JSON-like map. +func (opts UpdateOpts) ToNodeUpdateMap() (map[string]interface{}, error) { + node := make(map[string]interface{}) + + if opts.Condition != "" { + node["condition"] = opts.Condition + } + if opts.Weight != nil { + if weightErr := validateWeight(opts.Weight); weightErr != nil { + return node, weightErr + } + node["weight"] = &opts.Weight + } + if opts.Type != "" { + node["type"] = opts.Type + } + + return map[string]interface{}{"node": node}, nil +} + +// Update is the operation responsible for updating an existing node. A node's +// IP, port, and status are immutable attributes and cannot be modified. +func Update(c *gophercloud.ServiceClient, lbID, nodeID int, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToNodeUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", resourceURL(c, lbID, nodeID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + OkCodes: []int{202}, + }) + + return res +} + +// Delete is the operation responsible for permanently deleting a node. +func Delete(c *gophercloud.ServiceClient, lbID, nodeID int) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, nodeID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} + +// ListEventsOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListEventsOptsBuilder interface { + ToEventsListQuery() (string, error) +} + +// ListEventsOpts allows the filtering and sorting of paginated collections through +// the API. +type ListEventsOpts struct { + Marker string `q:"marker"` + Limit int `q:"limit"` +} + +// ToEventsListQuery formats a ListOpts into a query string. +func (opts ListEventsOpts) ToEventsListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListEvents is the operation responsible for listing all the events +// associated with the activity between the node and the load balancer. The +// events report errors found with the node. The detailedMessage provides the +// detailed reason for the error. +func ListEvents(client *gophercloud.ServiceClient, loadBalancerID int, opts ListEventsOptsBuilder) pagination.Pager { + url := eventsURL(client, loadBalancerID) + + if opts != nil { + query, err := opts.ToEventsListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return NodeEventPage{pagination.SinglePageBase(r)} + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go new file mode 100644 index 00000000000..003d347c073 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests_test.go @@ -0,0 +1,211 @@ +package nodes + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + lbID = 12345 + nodeID = 67890 + nodeID2 = 67891 +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListResponse(t, lbID) + + count := 0 + + err := List(client.ServiceClient(), lbID, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNodes(page) + th.AssertNoErr(t, err) + + expected := []Node{ + Node{ + ID: 410, + Address: "10.1.1.1", + Port: 80, + Condition: ENABLED, + Status: ONLINE, + Weight: 3, + Type: PRIMARY, + }, + Node{ + ID: 411, + Address: "10.1.1.2", + Port: 80, + Condition: ENABLED, + Status: ONLINE, + Weight: 8, + Type: SECONDARY, + }, + } + + 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() + + mockCreateResponse(t, lbID) + + opts := CreateOpts{ + CreateOpt{ + Address: "10.2.2.3", + Port: 80, + Condition: ENABLED, + Type: PRIMARY, + }, + CreateOpt{ + Address: "10.2.2.4", + Port: 81, + Condition: ENABLED, + Type: SECONDARY, + }, + } + + page := Create(client.ServiceClient(), lbID, opts) + + actual, err := page.ExtractNodes() + th.AssertNoErr(t, err) + + expected := []Node{ + Node{ + ID: 185, + Address: "10.2.2.3", + Port: 80, + Condition: ENABLED, + Status: ONLINE, + Weight: 1, + Type: PRIMARY, + }, + Node{ + ID: 186, + Address: "10.2.2.4", + Port: 81, + Condition: ENABLED, + Status: ONLINE, + Weight: 1, + Type: SECONDARY, + }, + } + + th.CheckDeepEquals(t, expected, actual) +} + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + ids := []int{nodeID, nodeID2} + + mockBatchDeleteResponse(t, lbID, ids) + + err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetResponse(t, lbID, nodeID) + + node, err := Get(client.ServiceClient(), lbID, nodeID).Extract() + th.AssertNoErr(t, err) + + expected := &Node{ + ID: 410, + Address: "10.1.1.1", + Port: 80, + Condition: ENABLED, + Status: ONLINE, + Weight: 12, + Type: PRIMARY, + } + + th.AssertDeepEquals(t, expected, node) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateResponse(t, lbID, nodeID) + + opts := UpdateOpts{ + Weight: gophercloud.IntToPointer(10), + Condition: DRAINING, + Type: SECONDARY, + } + + err := Update(client.ServiceClient(), lbID, nodeID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID, nodeID) + + err := Delete(client.ServiceClient(), lbID, nodeID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestListEvents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListEventsResponse(t, lbID) + + count := 0 + + pager := ListEvents(client.ServiceClient(), lbID, ListEventsOpts{}) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNodeEvents(page) + th.AssertNoErr(t, err) + + expected := []NodeEvent{ + NodeEvent{ + DetailedMessage: "Node is ok", + NodeID: 373, + ID: 7, + Type: "UPDATE_NODE", + Description: "Node '373' status changed to 'ONLINE' for load balancer '323'", + Category: "UPDATE", + Severity: "INFO", + RelativeURI: "/406271/loadbalancers/323/nodes/373/events", + AccountID: 406271, + LoadBalancerID: 323, + Title: "Node Status Updated", + Author: "Rackspace Cloud", + Created: "10-30-2012 10:18:23", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go new file mode 100644 index 00000000000..916485f2fcb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/results.go @@ -0,0 +1,210 @@ +package nodes + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Node represents a back-end device, usually a virtual machine, that can +// handle traffic. It is assigned traffic based on its parent load balancer. +type Node struct { + // The IP address or CIDR for this back-end node. + Address string + + // The unique ID for this node. + ID int + + // The port on which traffic is sent and received. + Port int + + // The node's status. + Status Status + + // The node's condition. + Condition Condition + + // The priority at which this node will receive traffic if a weighted + // algorithm is used by its parent load balancer. Ranges from 1 to 100. + Weight int + + // Type of node. + Type Type +} + +// Type indicates whether the node is of a PRIMARY or SECONDARY nature. +type Type string + +const ( + // PRIMARY nodes are in the normal rotation to receive traffic from the load + // balancer. + PRIMARY Type = "PRIMARY" + + // SECONDARY nodes are only in the rotation to receive traffic from the load + // balancer when all the primary nodes fail. This provides a failover feature + // that automatically routes traffic to the secondary node in the event that + // the primary node is disabled or in a failing state. Note that active + // health monitoring must be enabled on the load balancer to enable the + // failover feature to the secondary node. + SECONDARY Type = "SECONDARY" +) + +// Condition represents the condition of a node. +type Condition string + +const ( + // ENABLED indicates that the node is permitted to accept new connections. + ENABLED Condition = "ENABLED" + + // DISABLED indicates that the node is not permitted to accept any new + // connections regardless of session persistence configuration. Existing + // connections are forcibly terminated. + DISABLED Condition = "DISABLED" + + // DRAINING indicates that the node is allowed to service existing + // established connections and connections that are being directed to it as a + // result of the session persistence configuration. + DRAINING Condition = "DRAINING" +) + +// Status indicates whether the node can accept service traffic. If a node is +// not listening on its port or does not meet the conditions of the defined +// active health check for the load balancer, then the load balancer does not +// forward connections, and its status is listed as OFFLINE. +type Status string + +const ( + // ONLINE indicates that the node is healthy and capable of receiving traffic + // from the load balancer. + ONLINE Status = "ONLINE" + + // OFFLINE indicates that the node is not in a position to receive service + // traffic. It is usually switched into this state when a health check is not + // satisfied with the node's response time. + OFFLINE Status = "OFFLINE" +) + +// NodePage is the page returned by a pager when traversing over a collection +// of nodes. +type NodePage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether a NodePage struct is empty. +func (p NodePage) IsEmpty() (bool, error) { + is, err := ExtractNodes(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +func commonExtractNodes(body interface{}) ([]Node, error) { + var resp struct { + Nodes []Node `mapstructure:"nodes" json:"nodes"` + } + + err := mapstructure.Decode(body, &resp) + + return resp.Nodes, err +} + +// ExtractNodes accepts a Page struct, specifically a NodePage struct, and +// extracts the elements into a slice of Node structs. In other words, a +// generic collection is mapped into a relevant slice. +func ExtractNodes(page pagination.Page) ([]Node, error) { + return commonExtractNodes(page.(NodePage).Body) +} + +// CreateResult represents the result of a create operation. Since multiple +// nodes can be added in one operation, this result represents multiple nodes +// and should be treated as a typical pagination Page. Use its ExtractNodes +// method to get out a slice of Node structs. +type CreateResult struct { + pagination.SinglePageBase +} + +// ExtractNodes extracts a slice of Node structs from a CreateResult. +func (res CreateResult) ExtractNodes() ([]Node, error) { + return commonExtractNodes(res.Body) +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +type commonResult struct { + gophercloud.Result +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +func (r commonResult) Extract() (*Node, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Node Node `mapstructure:"node"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.Node, err +} + +// NodeEvent represents a service event that occurred between a node and a +// load balancer. +type NodeEvent struct { + ID int + DetailedMessage string + NodeID int + Type string + Description string + Category string + Severity string + RelativeURI string + AccountID int + LoadBalancerID int + Title string + Author string + Created string +} + +// NodeEventPage is a concrete type which embeds the common SinglePageBase +// struct, and is used when traversing node event collections. +type NodeEventPage struct { + pagination.SinglePageBase +} + +// IsEmpty is a concrete function which indicates whether an NodeEventPage is +// empty or not. +func (r NodeEventPage) IsEmpty() (bool, error) { + is, err := ExtractNodeEvents(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractNodeEvents accepts a Page struct, specifically a NodeEventPage +// struct, and extracts the elements into a slice of NodeEvent structs. In +// other words, the collection is mapped into a relevant slice. +func ExtractNodeEvents(page pagination.Page) ([]NodeEvent, error) { + var resp struct { + Events []NodeEvent `mapstructure:"nodeServiceEvents" json:"nodeServiceEvents"` + } + + err := mapstructure.Decode(page.(NodeEventPage).Body, &resp) + + return resp.Events, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/urls.go new file mode 100644 index 00000000000..2cefee26449 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/urls.go @@ -0,0 +1,25 @@ +package nodes + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + lbPath = "loadbalancers" + nodePath = "nodes" + eventPath = "events" +) + +func resourceURL(c *gophercloud.ServiceClient, lbID, nodeID int) string { + return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath, strconv.Itoa(nodeID)) +} + +func rootURL(c *gophercloud.ServiceClient, lbID int) string { + return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath) +} + +func eventsURL(c *gophercloud.ServiceClient, lbID int) string { + return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath, eventPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/doc.go new file mode 100644 index 00000000000..dcec0a87e2b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/doc.go @@ -0,0 +1,30 @@ +/* +Package sessions provides information and interaction with the Session +Persistence feature of the Rackspace Cloud Load Balancer service. + +Session persistence is a feature of the load balancing service that forces +multiple requests from clients (of the same protocol) to be directed to the +same node. This is common with many web applications that do not inherently +share application state between back-end servers. + +There are two modes to choose from: HTTP_COOKIE and SOURCE_IP. You can only set +one of the session persistence modes on a load balancer, and it can only +support one protocol. If you set HTTP_COOKIE mode for an HTTP load balancer, it +supports session persistence for HTTP requests only. Likewise, if you set +SOURCE_IP mode for an HTTPS load balancer, it supports session persistence for +only HTTPS requests. + +To support session persistence for both HTTP and HTTPS requests concurrently, +choose one of the following options: + +- Use two load balancers, one configured for session persistence for HTTP +requests and the other configured for session persistence for HTTPS requests. +That way, the load balancers support session persistence for both HTTP and +HTTPS requests concurrently, with each load balancer supporting one of the +protocols. + +- Use one load balancer, configure it for session persistence for HTTP requests, +and then enable SSL termination for that load balancer. The load balancer +supports session persistence for both HTTP and HTTPS requests concurrently. +*/ +package sessions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go new file mode 100644 index 00000000000..9596819d16d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/fixtures.go @@ -0,0 +1,58 @@ +package sessions + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(id int) string { + return "/loadbalancers/" + strconv.Itoa(id) + "/sessionpersistence" +} + +func mockGetResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "sessionPersistence": { + "persistenceType": "HTTP_COOKIE" + } +} +`) + }) +} + +func mockEnableResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "sessionPersistence": { + "persistenceType": "HTTP_COOKIE" + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDisableResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests.go new file mode 100644 index 00000000000..9853ad1320c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests.go @@ -0,0 +1,82 @@ +package sessions + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. +type CreateOptsBuilder interface { + ToSPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Required - can either be HTTPCOOKIE or SOURCEIP + Type Type +} + +// ToSPCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToSPCreateMap() (map[string]interface{}, error) { + sp := make(map[string]interface{}) + + if opts.Type == "" { + return sp, errors.New("Type is a required field") + } + + sp["persistenceType"] = opts.Type + return map[string]interface{}{"sessionPersistence": sp}, nil +} + +// Enable is the operation responsible for enabling session persistence for a +// particular load balancer. +func Enable(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) EnableResult { + var res EnableResult + + reqBody, err := opts.ToSPCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + + return res +} + +// Get is the operation responsible for showing details of the session +// persistence configuration for a particular load balancer. +func Get(c *gophercloud.ServiceClient, lbID int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Disable is the operation responsible for disabling session persistence for a +// particular load balancer. +func Disable(c *gophercloud.ServiceClient, lbID int) DisableResult { + var res DisableResult + + _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests_test.go new file mode 100644 index 00000000000..f319e540bb3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests_test.go @@ -0,0 +1,44 @@ +package sessions + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const lbID = 12345 + +func TestEnable(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockEnableResponse(t, lbID) + + opts := CreateOpts{Type: HTTPCOOKIE} + err := Enable(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetResponse(t, lbID) + + sp, err := Get(client.ServiceClient(), lbID).Extract() + th.AssertNoErr(t, err) + + expected := &SessionPersistence{Type: HTTPCOOKIE} + th.AssertDeepEquals(t, expected, sp) +} + +func TestDisable(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDisableResponse(t, lbID) + + err := Disable(client.ServiceClient(), lbID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/results.go new file mode 100644 index 00000000000..fe90e722cbd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/results.go @@ -0,0 +1,58 @@ +package sessions + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" +) + +// Type represents the type of session persistence being used. +type Type string + +const ( + // HTTPCOOKIE is a session persistence mechanism that inserts an HTTP cookie + // and is used to determine the destination back-end node. This is supported + // for HTTP load balancing only. + HTTPCOOKIE Type = "HTTP_COOKIE" + + // SOURCEIP is a session persistence mechanism that keeps track of the source + // IP address that is mapped and is able to determine the destination + // back-end node. This is supported for HTTPS pass-through and non-HTTP load + // balancing only. + SOURCEIP Type = "SOURCE_IP" +) + +// SessionPersistence indicates how a load balancer is using session persistence +type SessionPersistence struct { + Type Type `mapstructure:"persistenceType"` +} + +// EnableResult represents the result of an enable operation. +type EnableResult struct { + gophercloud.ErrResult +} + +// DisableResult represents the result of a disable operation. +type DisableResult struct { + gophercloud.ErrResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as an SP, if possible. +func (r GetResult) Extract() (*SessionPersistence, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + SP SessionPersistence `mapstructure:"sessionPersistence"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.SP, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/urls.go new file mode 100644 index 00000000000..c4a896d9057 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/urls.go @@ -0,0 +1,16 @@ +package sessions + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + spPath = "sessionpersistence" +) + +func rootURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), spPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/doc.go new file mode 100644 index 00000000000..6a2c174ae93 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/doc.go @@ -0,0 +1,22 @@ +/* +Package ssl provides information and interaction with the SSL Termination +feature of the Rackspace Cloud Load Balancer service. + +You may only enable and configure SSL termination on load balancers with +non-secure protocols, such as HTTP, but not HTTPS. + +SSL-terminated load balancers decrypt the traffic at the traffic manager and +pass unencrypted traffic to the back-end node. Because of this, the customer's +back-end nodes don't know what protocol the client requested. For this reason, +the X-Forwarded-Proto (XFP) header has been added for identifying the +originating protocol of an HTTP request as "http" or "https" depending on what +protocol the client requested. + +Not every service returns certificates in the proper order. Please verify that +your chain of certificates matches that of walking up the chain from the domain +to the CA root. + +If used for HTTP to HTTPS redirection, the LoadBalancer's securePort attribute +must be set to 443, and its secureTrafficOnly attribute must be true. +*/ +package ssl diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go new file mode 100644 index 00000000000..1d401001250 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/fixtures.go @@ -0,0 +1,195 @@ +package ssl + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(id int) string { + return "/loadbalancers/" + strconv.Itoa(id) + "/ssltermination" +} + +func _certURL(id, certID int) string { + url := _rootURL(id) + "/certificatemappings" + if certID > 0 { + url += "/" + strconv.Itoa(certID) + } + return url +} + +func mockGetResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "sslTermination": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "enabled": true, + "secureTrafficOnly": false, + "intermediateCertificate": "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + "securePort": 443 + } +} +`) + }) +} + +func mockUpdateResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "sslTermination": { + "enabled": true, + "securePort": 443, + "secureTrafficOnly": false, + "privateKey": "foo", + "certificate": "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate": "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} + `) + + w.WriteHeader(http.StatusOK) + }) +} + +func mockDeleteResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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) + }) +} + +func mockListCertsResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_certURL(lbID, 0), 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, ` +{ + "certificateMappings": [ + { + "certificateMapping": { + "id": 123, + "hostName": "rackspace.com" + } + }, + { + "certificateMapping": { + "id": 124, + "hostName": "*.rackspace.com" + } + } + ] +} +`) + }) +} + +func mockAddCertResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_certURL(lbID, 0), 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, ` +{ + "certificateMapping": { + "hostName": "rackspace.com", + "privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n", + "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} + `) + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "certificateMapping": { + "id": 123, + "hostName": "rackspace.com", + "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} + `) + }) +} + +func mockGetCertResponse(t *testing.T, lbID, certID int) { + th.Mux.HandleFunc(_certURL(lbID, certID), 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, ` +{ + "certificateMapping": { + "id": 123, + "hostName": "rackspace.com", + "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} +`) + }) +} + +func mockUpdateCertResponse(t *testing.T, lbID, certID int) { + th.Mux.HandleFunc(_certURL(lbID, certID), 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, ` +{ + "certificateMapping": { + "hostName": "rackspace.com", + "privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n", + "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} + `) + + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "certificateMapping": { + "id": 123, + "hostName": "rackspace.com", + "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n" + } +} + `) + }) +} + +func mockDeleteCertResponse(t *testing.T, lbID, certID int) { + th.Mux.HandleFunc(_certURL(lbID, certID), 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/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests.go new file mode 100644 index 00000000000..84b27121729 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests.go @@ -0,0 +1,278 @@ +package ssl + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +var ( + errPrivateKey = errors.New("PrivateKey is a required field") + errCertificate = errors.New("Certificate is a required field") + errIntCertificate = errors.New("IntCertificate is a required field") +) + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. +type UpdateOptsBuilder interface { + ToSSLUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Required - consult the SSLTermConfig struct for more info. + SecurePort int + + // Required - consult the SSLTermConfig struct for more info. + PrivateKey string + + // Required - consult the SSLTermConfig struct for more info. + Certificate string + + // Required - consult the SSLTermConfig struct for more info. + IntCertificate string + + // Optional - consult the SSLTermConfig struct for more info. + Enabled *bool + + // Optional - consult the SSLTermConfig struct for more info. + SecureTrafficOnly *bool +} + +// ToSSLUpdateMap casts a CreateOpts struct to a map. +func (opts UpdateOpts) ToSSLUpdateMap() (map[string]interface{}, error) { + ssl := make(map[string]interface{}) + + if opts.SecurePort == 0 { + return ssl, errors.New("SecurePort needs to be an integer greater than 0") + } + if opts.PrivateKey == "" { + return ssl, errPrivateKey + } + if opts.Certificate == "" { + return ssl, errCertificate + } + if opts.IntCertificate == "" { + return ssl, errIntCertificate + } + + ssl["securePort"] = opts.SecurePort + ssl["privateKey"] = opts.PrivateKey + ssl["certificate"] = opts.Certificate + ssl["intermediateCertificate"] = opts.IntCertificate + + if opts.Enabled != nil { + ssl["enabled"] = &opts.Enabled + } + + if opts.SecureTrafficOnly != nil { + ssl["secureTrafficOnly"] = &opts.SecureTrafficOnly + } + + return map[string]interface{}{"sslTermination": ssl}, nil +} + +// Update is the operation responsible for updating the SSL Termination +// configuration for a load balancer. +func Update(c *gophercloud.ServiceClient, lbID int, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToSSLUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Get is the operation responsible for showing the details of the SSL +// Termination configuration for a load balancer. +func Get(c *gophercloud.ServiceClient, lbID int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Delete is the operation responsible for deleting the SSL Termination +// configuration for a load balancer. +func Delete(c *gophercloud.ServiceClient, lbID int) DeleteResult { + var res DeleteResult + + _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return res +} + +// ListCerts will list all of the certificate mappings associated with a +// SSL-terminated HTTP load balancer. +func ListCerts(c *gophercloud.ServiceClient, lbID int) pagination.Pager { + url := certURL(c, lbID) + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return CertPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateCertOptsBuilder is the interface options structs have to satisfy in +// order to be used in the AddCert operation in this package. +type CreateCertOptsBuilder interface { + ToCertCreateMap() (map[string]interface{}, error) +} + +// CreateCertOpts represents the options used when adding a new certificate mapping. +type CreateCertOpts struct { + HostName string + PrivateKey string + Certificate string + IntCertificate string +} + +// ToCertCreateMap will cast an CreateCertOpts struct to a map for JSON serialization. +func (opts CreateCertOpts) ToCertCreateMap() (map[string]interface{}, error) { + cm := make(map[string]interface{}) + + if opts.HostName == "" { + return cm, errors.New("HostName is a required option") + } + if opts.PrivateKey == "" { + return cm, errPrivateKey + } + if opts.Certificate == "" { + return cm, errCertificate + } + + cm["hostName"] = opts.HostName + cm["privateKey"] = opts.PrivateKey + cm["certificate"] = opts.Certificate + + if opts.IntCertificate != "" { + cm["intermediateCertificate"] = opts.IntCertificate + } + + return map[string]interface{}{"certificateMapping": cm}, nil +} + +// CreateCert will add a new SSL certificate and allow an SSL-terminated HTTP +// load balancer to use it. This feature is useful because it allows multiple +// certificates to be used. The maximum number of certificates that can be +// stored per LB is 20. +func CreateCert(c *gophercloud.ServiceClient, lbID int, opts CreateCertOptsBuilder) CreateCertResult { + var res CreateCertResult + + reqBody, err := opts.ToCertCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", certURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// GetCert will show the details of an existing SSL certificate. +func GetCert(c *gophercloud.ServiceClient, lbID, certID int) GetCertResult { + var res GetCertResult + + _, res.Err = perigee.Request("GET", certResourceURL(c, lbID, certID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// UpdateCertOptsBuilder is the interface options structs have to satisfy in +// order to be used in the UpdateCert operation in this package. +type UpdateCertOptsBuilder interface { + ToCertUpdateMap() (map[string]interface{}, error) +} + +// UpdateCertOpts represents the options needed to update a SSL certificate. +type UpdateCertOpts struct { + HostName string + PrivateKey string + Certificate string + IntCertificate string +} + +// ToCertUpdateMap will cast an UpdateCertOpts struct into a map for JSON +// seralization. +func (opts UpdateCertOpts) ToCertUpdateMap() (map[string]interface{}, error) { + cm := make(map[string]interface{}) + + if opts.HostName != "" { + cm["hostName"] = opts.HostName + } + if opts.PrivateKey != "" { + cm["privateKey"] = opts.PrivateKey + } + if opts.Certificate != "" { + cm["certificate"] = opts.Certificate + } + if opts.IntCertificate != "" { + cm["intermediateCertificate"] = opts.IntCertificate + } + + return map[string]interface{}{"certificateMapping": cm}, nil +} + +// UpdateCert is the operation responsible for updating the details of an +// existing SSL certificate. +func UpdateCert(c *gophercloud.ServiceClient, lbID, certID int, opts UpdateCertOptsBuilder) UpdateCertResult { + var res UpdateCertResult + + reqBody, err := opts.ToCertUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", certResourceURL(c, lbID, certID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + + return res +} + +// DeleteCert is the operation responsible for permanently removing a SSL +// certificate. +func DeleteCert(c *gophercloud.ServiceClient, lbID, certID int) DeleteResult { + var res DeleteResult + + _, res.Err = perigee.Request("DELETE", certResourceURL(c, lbID, certID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests_test.go new file mode 100644 index 00000000000..fb14c4a28d5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests_test.go @@ -0,0 +1,167 @@ +package ssl + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + lbID = 12345 + certID = 67890 +) + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetResponse(t, lbID) + + sp, err := Get(client.ServiceClient(), lbID).Extract() + th.AssertNoErr(t, err) + + expected := &SSLTermConfig{ + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + Enabled: true, + SecureTrafficOnly: false, + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + SecurePort: 443, + } + th.AssertDeepEquals(t, expected, sp) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateResponse(t, lbID) + + opts := UpdateOpts{ + Enabled: gophercloud.Enabled, + SecurePort: 443, + SecureTrafficOnly: gophercloud.Disabled, + PrivateKey: "foo", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + err := Update(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID) + + err := Delete(client.ServiceClient(), lbID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestListCerts(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListCertsResponse(t, lbID) + + count := 0 + + err := ListCerts(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractCerts(page) + th.AssertNoErr(t, err) + + expected := []Certificate{ + Certificate{ID: 123, HostName: "rackspace.com"}, + Certificate{ID: 124, HostName: "*.rackspace.com"}, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, count) +} + +func TestCreateCert(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockAddCertResponse(t, lbID) + + opts := CreateCertOpts{ + HostName: "rackspace.com", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + + cm, err := CreateCert(client.ServiceClient(), lbID, opts).Extract() + th.AssertNoErr(t, err) + + expected := &Certificate{ + ID: 123, + HostName: "rackspace.com", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + th.AssertDeepEquals(t, expected, cm) +} + +func TestGetCertMapping(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetCertResponse(t, lbID, certID) + + sp, err := GetCert(client.ServiceClient(), lbID, certID).Extract() + th.AssertNoErr(t, err) + + expected := &Certificate{ + ID: 123, + HostName: "rackspace.com", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + th.AssertDeepEquals(t, expected, sp) +} + +func TestUpdateCert(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockUpdateCertResponse(t, lbID, certID) + + opts := UpdateCertOpts{ + HostName: "rackspace.com", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + + cm, err := UpdateCert(client.ServiceClient(), lbID, certID, opts).Extract() + th.AssertNoErr(t, err) + + expected := &Certificate{ + ID: 123, + HostName: "rackspace.com", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n", + IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n", + } + th.AssertDeepEquals(t, expected, cm) +} + +func TestDeleteCert(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteCertResponse(t, lbID, certID) + + err := DeleteCert(client.ServiceClient(), lbID, certID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/results.go new file mode 100644 index 00000000000..ead9fcd37eb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/results.go @@ -0,0 +1,148 @@ +package ssl + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// SSLTermConfig represents the SSL configuration for a particular load balancer. +type SSLTermConfig struct { + // The port on which the SSL termination load balancer listens for secure + // traffic. The value must be unique to the existing LB protocol/port + // combination + SecurePort int `mapstructure:"securePort"` + + // The private key for the SSL certificate which is validated and verified + // against the provided certificates. + PrivateKey string `mapstructure:"privatekey"` + + // The certificate used for SSL termination, which is validated and verified + // against the key and intermediate certificate if provided. + Certificate string + + // The intermediate certificate (for the user). The intermediate certificate + // is validated and verified against the key and certificate credentials + // provided. A user may only provide this value when accompanied by a + // Certificate, PrivateKey, and SecurePort. It may not be added or updated as + // a single attribute in a future operation. + IntCertificate string `mapstructure:"intermediatecertificate"` + + // Determines if the load balancer is enabled to terminate SSL traffic or not. + // If this is set to false, the load balancer retains its specified SSL + // attributes but does not terminate SSL traffic. + Enabled bool + + // Determines if the load balancer can only accept secure traffic. If set to + // true, the load balancer will not accept non-secure traffic. + SecureTrafficOnly bool +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as a SSLTermConfig struct, if possible. +func (r GetResult) Extract() (*SSLTermConfig, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + SSL SSLTermConfig `mapstructure:"sslTermination"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.SSL, err +} + +// Certificate represents an SSL certificate associated with an SSL-terminated +// HTTP load balancer. +type Certificate struct { + ID int + HostName string + Certificate string + IntCertificate string `mapstructure:"intermediateCertificate"` +} + +// CertPage represents a page of certificates. +type CertPage struct { + pagination.LinkedPageBase +} + +// IsEmpty checks whether a CertMappingPage struct is empty. +func (p CertPage) IsEmpty() (bool, error) { + is, err := ExtractCerts(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractCerts accepts a Page struct, specifically a CertPage struct, and +// extracts the elements into a slice of Cert structs. In other words, a generic +// collection is mapped into a relevant slice. +func ExtractCerts(page pagination.Page) ([]Certificate, error) { + type NestedMap struct { + Cert Certificate `mapstructure:"certificateMapping" json:"certificateMapping"` + } + var resp struct { + Certs []NestedMap `mapstructure:"certificateMappings" json:"certificateMappings"` + } + + err := mapstructure.Decode(page.(CertPage).Body, &resp) + + slice := []Certificate{} + for _, cert := range resp.Certs { + slice = append(slice, cert.Cert) + } + + return slice, err +} + +type certResult struct { + gophercloud.Result +} + +// Extract interprets a result as a CertMapping struct, if possible. +func (r certResult) Extract() (*Certificate, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Cert Certificate `mapstructure:"certificateMapping"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.Cert, err +} + +// CreateCertResult represents the result of an CreateCert operation. +type CreateCertResult struct { + certResult +} + +// GetCertResult represents the result of a GetCert operation. +type GetCertResult struct { + certResult +} + +// UpdateCertResult represents the result of an UpdateCert operation. +type UpdateCertResult struct { + certResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/urls.go new file mode 100644 index 00000000000..aa814b35834 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/urls.go @@ -0,0 +1,25 @@ +package ssl + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + sslPath = "ssltermination" + certPath = "certificatemappings" +) + +func rootURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), sslPath) +} + +func certURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), sslPath, certPath) +} + +func certResourceURL(c *gophercloud.ServiceClient, id, certID int) string { + return c.ServiceURL(path, strconv.Itoa(id), sslPath, certPath, strconv.Itoa(certID)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/doc.go new file mode 100644 index 00000000000..1ed605d3629 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/doc.go @@ -0,0 +1,5 @@ +/* +Package throttle provides information and interaction with the Connection +Throttling feature of the Rackspace Cloud Load Balancer service. +*/ +package throttle diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go new file mode 100644 index 00000000000..40223f60a67 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/fixtures.go @@ -0,0 +1,61 @@ +package throttle + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(id int) string { + return "/loadbalancers/" + strconv.Itoa(id) + "/connectionthrottle" +} + +func mockGetResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "connectionThrottle": { + "maxConnections": 100 + } +} +`) + }) +} + +func mockCreateResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "connectionThrottle": { + "maxConnectionRate": 0, + "maxConnections": 200, + "minConnections": 0, + "rateInterval": 0 + } +} + `) + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests.go new file mode 100644 index 00000000000..8c2e4be415a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests.go @@ -0,0 +1,95 @@ +package throttle + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. +type CreateOptsBuilder interface { + ToCTCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Required - the maximum amount of connections per IP address to allow per LB. + MaxConnections int + + // Deprecated as of v1.22. + MaxConnectionRate int + + // Deprecated as of v1.22. + MinConnections int + + // Deprecated as of v1.22. + RateInterval int +} + +// ToCTCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToCTCreateMap() (map[string]interface{}, error) { + ct := make(map[string]interface{}) + + if opts.MaxConnections < 0 || opts.MaxConnections > 100000 { + return ct, errors.New("MaxConnections must be an int between 0 and 100000") + } + + ct["maxConnections"] = opts.MaxConnections + ct["maxConnectionRate"] = opts.MaxConnectionRate + ct["minConnections"] = opts.MinConnections + ct["rateInterval"] = opts.RateInterval + + return map[string]interface{}{"connectionThrottle": ct}, nil +} + +// Create is the operation responsible for creating or updating the connection +// throttling configuration for a load balancer. +func Create(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToCTCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + + return res +} + +// Get is the operation responsible for showing the details of the connection +// throttling configuration for a load balancer. +func Get(c *gophercloud.ServiceClient, lbID int) GetResult { + var res GetResult + + _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + Results: &res.Body, + OkCodes: []int{200}, + }) + + return res +} + +// Delete is the operation responsible for deleting the connection throttling +// configuration for a load balancer. +func Delete(c *gophercloud.ServiceClient, lbID int) DeleteResult { + var res DeleteResult + + _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests_test.go new file mode 100644 index 00000000000..6e9703ffce3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests_test.go @@ -0,0 +1,44 @@ +package throttle + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const lbID = 12345 + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockCreateResponse(t, lbID) + + opts := CreateOpts{MaxConnections: 200} + err := Create(client.ServiceClient(), lbID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockGetResponse(t, lbID) + + sp, err := Get(client.ServiceClient(), lbID).Extract() + th.AssertNoErr(t, err) + + expected := &ConnectionThrottle{MaxConnections: 100} + th.AssertDeepEquals(t, expected, sp) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID) + + err := Delete(client.ServiceClient(), lbID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/results.go new file mode 100644 index 00000000000..db93c6f3f4f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/results.go @@ -0,0 +1,43 @@ +package throttle + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" +) + +// ConnectionThrottle represents the connection throttle configuration for a +// particular load balancer. +type ConnectionThrottle struct { + MaxConnections int +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + gophercloud.ErrResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as a SP, if possible. +func (r GetResult) Extract() (*ConnectionThrottle, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + CT ConnectionThrottle `mapstructure:"connectionThrottle"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.CT, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/urls.go new file mode 100644 index 00000000000..b77f0ac1c79 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/urls.go @@ -0,0 +1,16 @@ +package throttle + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + path = "loadbalancers" + ctPath = "connectionthrottle" +) + +func rootURL(c *gophercloud.ServiceClient, id int) string { + return c.ServiceURL(path, strconv.Itoa(id), ctPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/doc.go new file mode 100644 index 00000000000..5c3846d44d6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/doc.go @@ -0,0 +1,13 @@ +/* +Package vips provides information and interaction with the Virtual IP API +resource for the Rackspace Cloud Load Balancer service. + +A virtual IP (VIP) makes a load balancer accessible by clients. The load +balancing service supports either a public VIP, routable on the public Internet, +or a ServiceNet address, routable only within the region in which the load +balancer resides. + +All load balancers must have at least one virtual IP associated with them at +all times. +*/ +package vips diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/fixtures.go new file mode 100644 index 00000000000..158759f7fa1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/fixtures.go @@ -0,0 +1,88 @@ +package vips + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func _rootURL(lbID int) string { + return "/loadbalancers/" + strconv.Itoa(lbID) + "/virtualips" +} + +func mockListResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "virtualIps": [ + { + "id": 1000, + "address": "206.10.10.210", + "type": "PUBLIC" + } + ] +} + `) + }) +} + +func mockCreateResponse(t *testing.T, lbID int) { + th.Mux.HandleFunc(_rootURL(lbID), 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, ` +{ + "type":"PUBLIC", + "ipVersion":"IPV6" +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "address":"fd24:f480:ce44:91bc:1af2:15ff:0000:0002", + "id":9000134, + "type":"PUBLIC", + "ipVersion":"IPV6" +} + `) + }) +} + +func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) { + th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + r.ParseForm() + + for k, v := range ids { + fids := r.Form["id"] + th.AssertEquals(t, strconv.Itoa(v), fids[k]) + } + + w.WriteHeader(http.StatusAccepted) + }) +} + +func mockDeleteResponse(t *testing.T, lbID, vipID int) { + url := _rootURL(lbID) + "/" + strconv.Itoa(vipID) + 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.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests.go new file mode 100644 index 00000000000..42f0c1d071a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests.go @@ -0,0 +1,112 @@ +package vips + +import ( + "errors" + + "github.com/racker/perigee" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List is the operation responsible for returning a paginated collection of +// load balancer virtual IP addresses. +func List(client *gophercloud.ServiceClient, loadBalancerID int) pagination.Pager { + url := rootURL(client, loadBalancerID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VIPPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToVIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Optional - the ID of an existing virtual IP. By doing this, you are + // allowing load balancers to share IPV6 addresses. + ID string + + // Optional - the type of address. + Type Type + + // Optional - the version of address. + Version Version +} + +// ToVIPCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToVIPCreateMap() (map[string]interface{}, error) { + lb := make(map[string]interface{}) + + if opts.ID != "" { + lb["id"] = opts.ID + } + if opts.Type != "" { + lb["type"] = opts.Type + } + if opts.Version != "" { + lb["ipVersion"] = opts.Version + } + + return lb, nil +} + +// Create is the operation responsible for assigning a new Virtual IP to an +// existing load balancer resource. Currently, only version 6 IP addresses may +// be added. +func Create(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToVIPCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = perigee.Request("POST", rootURL(c, lbID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &res.Body, + OkCodes: []int{202}, + }) + + return res +} + +// BulkDelete is the operation responsible for batch deleting multiple VIPs in +// a single operation. It accepts a slice of integer IDs and will remove them +// from the load balancer. The maximum limit is 10 VIP removals at once. +func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, vipIDs []int) DeleteResult { + var res DeleteResult + + if len(vipIDs) > 10 || len(vipIDs) == 0 { + res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 VIP IDs") + return res + } + + url := rootURL(c, loadBalancerID) + url += gophercloud.IDSliceToQueryString("id", vipIDs) + + _, res.Err = perigee.Request("DELETE", url, perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + + return res +} + +// Delete is the operation responsible for permanently deleting a VIP. +func Delete(c *gophercloud.ServiceClient, lbID, vipID int) DeleteResult { + var res DeleteResult + _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, vipID), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests_test.go new file mode 100644 index 00000000000..74ac4617381 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests_test.go @@ -0,0 +1,87 @@ +package vips + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const ( + lbID = 12345 + vipID = 67890 + vipID2 = 67891 +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockListResponse(t, lbID) + + count := 0 + + err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVIPs(page) + th.AssertNoErr(t, err) + + expected := []VIP{ + VIP{ID: 1000, Address: "206.10.10.210", Type: "PUBLIC"}, + } + + 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() + + mockCreateResponse(t, lbID) + + opts := CreateOpts{ + Type: "PUBLIC", + Version: "IPV6", + } + + vip, err := Create(client.ServiceClient(), lbID, opts).Extract() + th.AssertNoErr(t, err) + + expected := &VIP{ + Address: "fd24:f480:ce44:91bc:1af2:15ff:0000:0002", + ID: 9000134, + Type: "PUBLIC", + Version: "IPV6", + } + + th.CheckDeepEquals(t, expected, vip) +} + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + ids := []int{vipID, vipID2} + + mockBatchDeleteResponse(t, lbID, ids) + + err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + mockDeleteResponse(t, lbID, vipID) + + err := Delete(client.ServiceClient(), lbID, vipID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/results.go new file mode 100644 index 00000000000..678b2aff797 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/results.go @@ -0,0 +1,89 @@ +package vips + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// VIP represents a Virtual IP API resource. +type VIP struct { + Address string `json:"address,omitempty"` + ID int `json:"id,omitempty"` + Type Type `json:"type,omitempty"` + Version Version `json:"ipVersion,omitempty" mapstructure:"ipVersion"` +} + +// Version represents the version of a VIP. +type Version string + +// Convenient constants to use for type +const ( + IPV4 Version = "IPV4" + IPV6 Version = "IPV6" +) + +// Type represents the type of a VIP. +type Type string + +const ( + // PUBLIC indicates a VIP type that is routable on the public Internet. + PUBLIC Type = "PUBLIC" + + // PRIVATE indicates a VIP type that is routable only on ServiceNet. + PRIVATE Type = "SERVICENET" +) + +// VIPPage is the page returned by a pager when traversing over a collection +// of VIPs. +type VIPPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether a VIPPage struct is empty. +func (p VIPPage) IsEmpty() (bool, error) { + is, err := ExtractVIPs(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractVIPs accepts a Page struct, specifically a VIPPage struct, and +// extracts the elements into a slice of VIP structs. In other words, a +// generic collection is mapped into a relevant slice. +func ExtractVIPs(page pagination.Page) ([]VIP, error) { + var resp struct { + VIPs []VIP `mapstructure:"virtualIps" json:"virtualIps"` + } + + err := mapstructure.Decode(page.(VIPPage).Body, &resp) + + return resp.VIPs, err +} + +type commonResult struct { + gophercloud.Result +} + +func (r commonResult) Extract() (*VIP, error) { + if r.Err != nil { + return nil, r.Err + } + + resp := &VIP{} + err := mapstructure.Decode(r.Body, resp) + + return resp, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/urls.go new file mode 100644 index 00000000000..28f063a0f78 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/urls.go @@ -0,0 +1,20 @@ +package vips + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +const ( + lbPath = "loadbalancers" + vipPath = "virtualips" +) + +func resourceURL(c *gophercloud.ServiceClient, lbID, nodeID int) string { + return c.ServiceURL(lbPath, strconv.Itoa(lbID), vipPath, strconv.Itoa(nodeID)) +} + +func rootURL(c *gophercloud.ServiceClient, lbID int) string { + return c.ServiceURL(lbPath, strconv.Itoa(lbID), vipPath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/common/common_tests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/common/common_tests.go new file mode 100644 index 00000000000..129cd63aee9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/common/common_tests.go @@ -0,0 +1,12 @@ +package common + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const TokenID = client.TokenID + +func ServiceClient() *gophercloud.ServiceClient { + return client.ServiceClient() +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate.go new file mode 100644 index 00000000000..dcb0855dba3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate.go @@ -0,0 +1,41 @@ +package networks + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager which allows you to iterate over a collection of +// networks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// Get retrieves a specific network based on its unique ID. +func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult { + return os.Get(c, networkID) +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +// +// The tenant ID that is contained in the URI is the tenant that creates the +// network. An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, opts) +} + +// Update accepts a UpdateOpts struct and updates an existing network using the +// values provided. For more information, see the Create function. +func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, networkID, opts) +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult { + return os.Delete(c, networkID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go new file mode 100644 index 00000000000..f51c732d436 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/networks/delegate_test.go @@ -0,0 +1,276 @@ +package networks + +import ( + "fmt" + "net/http" + "testing" + + os "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks", 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, ` +{ + "networks": [ + { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + }, + { + "status": "ACTIVE", + "subnets": [ + "08eae331-0402-425a-923c-34f7cfe39c1b" + ], + "name": "private", + "admin_state_up": true, + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "shared": true, + "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324" + } + ] +} + `) + }) + + client := fake.ServiceClient() + count := 0 + + List(client, os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []os.Network{ + os.Network{ + Status: "ACTIVE", + Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}, + Name: "private-network", + AdminStateUp: true, + TenantID: "4fd44f30292945e481c7b8a0c8908869", + Shared: true, + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + }, + os.Network{ + Status: "ACTIVE", + Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"}, + Name: "private", + AdminStateUp: true, + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + Shared: true, + ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", 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, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}) + th.AssertEquals(t, n.Name, "private-network") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertEquals(t, n.Shared, true) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks", 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, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [], + "name": "net1", + "admin_state_up": true, + "tenant_id": "9bacb3c5d39d41a79512987f338cf177", + "shared": false, + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + iTrue := true + options := os.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue} + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{}) + th.AssertEquals(t, n.Name, "net1") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "9bacb3c5d39d41a79512987f338cf177") + th.AssertEquals(t, n.Shared, false) + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestCreateWithOptionalFields(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks", 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, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "sample_network", + "admin_state_up": true, + "shared": true, + "tenant_id": "12345" + } +} + `) + + w.WriteHeader(http.StatusCreated) + }) + + iTrue := true + options := os.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue, Shared: &iTrue, TenantID: "12345"} + _, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", 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, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "network": { + "name": "new_network_name", + "admin_state_up": false, + "shared": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "subnets": [], + "name": "new_network_name", + "admin_state_up": false, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + iTrue := true + options := os.UpdateOpts{Name: "new_network_name", AdminStateUp: os.Down, Shared: &iTrue} + n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "new_network_name") + th.AssertEquals(t, n.AdminStateUp, false) + th.AssertEquals(t, n.Shared, true) + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", 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 := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate.go new file mode 100644 index 00000000000..091b99e0f4d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate.go @@ -0,0 +1,40 @@ +package ports + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager which allows you to iterate over a collection of +// ports. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those ports that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// Get retrieves a specific port based on its unique ID. +func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult { + return os.Get(c, networkID) +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. You must remember to provide a NetworkID value. +func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, opts) +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, networkID, opts) +} + +// Delete accepts a unique ID and deletes the port associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult { + return os.Delete(c, networkID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate_test.go new file mode 100644 index 00000000000..f53ff595a04 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate_test.go @@ -0,0 +1,322 @@ +package ports + +import ( + "fmt" + "net/http" + "testing" + + os "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/pagination" + fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ports", 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, ` +{ + "ports": [ + { + "status": "ACTIVE", + "binding:host_id": "devstack", + "name": "", + "admin_state_up": true, + "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3", + "tenant_id": "", + "device_owner": "network:router_gateway", + "mac_address": "fa:16:3e:58:42:ed", + "fixed_ips": [ + { + "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", + "ip_address": "172.24.4.2" + } + ], + "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", + "security_groups": [], + "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractPorts(page) + if err != nil { + t.Errorf("Failed to extract subnets: %v", err) + return false, nil + } + + expected := []os.Port{ + os.Port{ + Status: "ACTIVE", + Name: "", + AdminStateUp: true, + NetworkID: "70c1db1f-b701-45bd-96e0-a313ee3430b3", + TenantID: "", + DeviceOwner: "network:router_gateway", + MACAddress: "fa:16:3e:58:42:ed", + FixedIPs: []os.IP{ + os.IP{ + SubnetID: "008ba151-0b8c-4a67-98b5-0d2b87666062", + IPAddress: "172.24.4.2", + }, + }, + ID: "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", + SecurityGroups: []string{}, + DeviceID: "9ae135f4-b6e0-4dad-9e91-3c223e385824", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", 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, ` +{ + "port": { + "status": "ACTIVE", + "name": "", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "7e02058126cc4950b75f9970368ba177", + "device_owner": "network:router_interface", + "mac_address": "fa:16:3e:23:fd:d7", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.1" + } + ], + "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", + "security_groups": [], + "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertEquals(t, n.Name, "") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") + th.AssertEquals(t, n.TenantID, "7e02058126cc4950b75f9970368ba177") + th.AssertEquals(t, n.DeviceOwner, "network:router_interface") + th.AssertEquals(t, n.MACAddress, "fa:16:3e:23:fd:d7") + th.AssertDeepEquals(t, n.FixedIPs, []os.IP{ + os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.1"}, + }) + th.AssertEquals(t, n.ID, "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2") + th.AssertDeepEquals(t, n.SecurityGroups, []string{}) + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ports", 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, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "name": "private-port", + "admin_state_up": true, + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.2" + } + ], + "security_groups": ["foo"] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "private-port", + "allowed_address_pairs": [], + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.2" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "device_id": "" + } +} + `) + }) + + asu := true + options := os.CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []os.IP{ + os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }, + SecurityGroups: []string{"foo"}, + } + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "DOWN") + th.AssertEquals(t, n.Name, "private-port") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") + th.AssertEquals(t, n.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") + th.AssertEquals(t, n.DeviceOwner, "") + th.AssertEquals(t, n.MACAddress, "fa:16:3e:c9:cb:f0") + th.AssertDeepEquals(t, n.FixedIPs, []os.IP{ + os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }) + th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d") + th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), os.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", 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, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "port": { + "name": "new_port_name", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "port": { + "status": "DOWN", + "name": "new_port_name", + "admin_state_up": true, + "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", + "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa", + "device_owner": "", + "mac_address": "fa:16:3e:c9:cb:f0", + "fixed_ips": [ + { + "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2", + "ip_address": "10.0.0.3" + } + ], + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "security_groups": [ + "f0ac4394-7e4a-4409-9701-ba8be283dbc3" + ], + "device_id": "" + } +} + `) + }) + + options := os.UpdateOpts{ + Name: "new_port_name", + FixedIPs: []os.IP{ + os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }, + SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}, + } + + s, err := Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "new_port_name") + th.AssertDeepEquals(t, s.FixedIPs, []os.IP{ + os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }) + th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"}) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", 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 := Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate.go new file mode 100644 index 00000000000..a7fb7bb15fc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate.go @@ -0,0 +1,40 @@ +package subnets + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager which allows you to iterate over a collection of +// subnets. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those subnets that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// Get retrieves a specific subnet based on its unique ID. +func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult { + return os.Get(c, networkID) +} + +// Create accepts a CreateOpts struct and creates a new subnet using the values +// provided. You must remember to provide a valid NetworkID, CIDR and IP version. +func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, opts) +} + +// Update accepts a UpdateOpts struct and updates an existing subnet using the +// values provided. +func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, networkID, opts) +} + +// Delete accepts a unique ID and deletes the subnet associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult { + return os.Delete(c, networkID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate_test.go new file mode 100644 index 00000000000..fafc6fb3020 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/subnets/delegate_test.go @@ -0,0 +1,363 @@ +package subnets + +import ( + "fmt" + "net/http" + "testing" + + os "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/subnets", 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, ` +{ + "subnets": [ + { + "name": "private-subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + }, + { + "name": "my_subnet", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.0.0.2", + "end": "192.255.255.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.0.0.1", + "cidr": "192.0.0.0/8", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractSubnets(page) + if err != nil { + t.Errorf("Failed to extract subnets: %v", err) + return false, nil + } + + expected := []os.Subnet{ + os.Subnet{ + Name: "private-subnet", + EnableDHCP: true, + NetworkID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + DNSNameservers: []string{}, + AllocationPools: []os.AllocationPool{ + os.AllocationPool{ + Start: "10.0.0.2", + End: "10.0.0.254", + }, + }, + HostRoutes: []os.HostRoute{}, + IPVersion: 4, + GatewayIP: "10.0.0.1", + CIDR: "10.0.0.0/24", + ID: "08eae331-0402-425a-923c-34f7cfe39c1b", + }, + os.Subnet{ + Name: "my_subnet", + EnableDHCP: true, + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + TenantID: "4fd44f30292945e481c7b8a0c8908869", + DNSNameservers: []string{}, + AllocationPools: []os.AllocationPool{ + os.AllocationPool{ + Start: "192.0.0.2", + End: "192.255.255.254", + }, + }, + HostRoutes: []os.HostRoute{}, + IPVersion: 4, + GatewayIP: "192.0.0.1", + CIDR: "192.0.0.0/8", + ID: "54d6f61d-db07-451c-9ab3-b9609b6b6f0b", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", 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, ` +{ + "subnet": { + "name": "my_subnet", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.0.0.2", + "end": "192.255.255.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.0.0.1", + "cidr": "192.0.0.0/8", + "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + } +} + `) + }) + + s, err := Get(fake.ServiceClient(), "54d6f61d-db07-451c-9ab3-b9609b6b6f0b").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_subnet") + th.AssertEquals(t, s.EnableDHCP, true) + th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.AllocationPools, []os.AllocationPool{ + os.AllocationPool{ + Start: "192.0.0.2", + End: "192.255.255.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []os.HostRoute{}) + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.GatewayIP, "192.0.0.1") + th.AssertEquals(t, s.CIDR, "192.0.0.0/8") + th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0b") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/subnets", 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, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "subnet": { + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "ip_version": 4, + "cidr": "192.168.199.0/24", + "dns_nameservers": ["foo"], + "allocation_pools": [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ], + "host_routes": [{"destination":"","nexthop": "bar"}] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "subnet": { + "name": "", + "enable_dhcp": true, + "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "192.168.199.1", + "cidr": "192.168.199.0/24", + "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126" + } +} + `) + }) + + opts := os.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + IPVersion: 4, + CIDR: "192.168.199.0/24", + AllocationPools: []os.AllocationPool{ + os.AllocationPool{ + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }, + DNSNameservers: []string{"foo"}, + HostRoutes: []os.HostRoute{ + os.HostRoute{NextHop: "bar"}, + }, + } + s, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "") + th.AssertEquals(t, s.EnableDHCP, true) + th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") + th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.AllocationPools, []os.AllocationPool{ + os.AllocationPool{ + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []os.HostRoute{}) + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.GatewayIP, "192.168.199.1") + th.AssertEquals(t, s.CIDR, "192.168.199.0/24") + th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126") +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), os.CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + res = Create(fake.ServiceClient(), os.CreateOpts{NetworkID: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + res = Create(fake.ServiceClient(), os.CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", 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, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "subnet": { + "name": "my_new_subnet", + "dns_nameservers": ["foo"], + "host_routes": [{"destination":"","nexthop": "bar"}] + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "subnet": { + "name": "my_new_subnet", + "enable_dhcp": true, + "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e", + "dns_nameservers": [], + "allocation_pools": [ + { + "start": "10.0.0.2", + "end": "10.0.0.254" + } + ], + "host_routes": [], + "ip_version": 4, + "gateway_ip": "10.0.0.1", + "cidr": "10.0.0.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + } +} + `) + }) + + opts := os.UpdateOpts{ + Name: "my_new_subnet", + DNSNameservers: []string{"foo"}, + HostRoutes: []os.HostRoute{ + os.HostRoute{NextHop: "bar"}, + }, + } + s, err := Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.Name, "my_new_subnet") + th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", 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 := Delete(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go index c568bd6e3b3..a1ea98bb700 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go @@ -8,20 +8,22 @@ import ( fake "github.com/rackspace/gophercloud/testhelper/client" ) -func TestGetAccounts(t *testing.T) { +func TestUpdateAccounts(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - os.HandleGetAccountSuccessfully(t) + + os.HandleUpdateAccountSuccessfully(t) options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}} res := Update(fake.ServiceClient(), options) th.CheckNoErr(t, res.Err) } -func TestUpdateAccounts(t *testing.T) { +func TestGetAccounts(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - os.HandleUpdateAccountSuccessfully(t) + + os.HandleGetAccountSuccessfully(t) expected := map[string]string{"Foo": "bar"} actual, err := Get(fake.ServiceClient()).ExtractMetadata() diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go index d7eef20255d..89adb83965d 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go @@ -1,8 +1,6 @@ package cdncontainers import ( - "strconv" - "github.com/rackspace/gophercloud" os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" "github.com/rackspace/gophercloud/pagination" @@ -38,34 +36,3 @@ func (opts ListOpts) ToContainerListParams() (bool, string, error) { func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { return os.List(c, opts) } - -// Get is a function that retrieves the metadata of a container. To extract just -// the custom metadata, pass the GetResult response to the ExtractMetadata -// function. -func Get(c *gophercloud.ServiceClient, containerName string) os.GetResult { - return os.Get(c, containerName) -} - -// UpdateOpts is a structure that holds parameters for updating, creating, or -// deleting a container's metadata. -type UpdateOpts struct { - CDNEnabled bool `h:"X-Cdn-Enabled"` - LogRetention bool `h:"X-Log-Retention"` - TTL int `h:"X-Ttl"` -} - -// ToContainerUpdateMap formats a CreateOpts into a map of headers. -func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) { - h, err := gophercloud.BuildHeaders(opts) - if err != nil { - return nil, err - } - h["X-Cdn-Enabled"] = strconv.FormatBool(opts.CDNEnabled) - return h, nil -} - -// Update is a function that creates, updates, or deletes a container's -// metadata. -func Update(c *gophercloud.ServiceClient, containerName string, opts os.UpdateOptsBuilder) os.UpdateResult { - return os.Update(c, containerName, opts) -} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go index 0567833204c..05b19399338 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go @@ -1,6 +1,8 @@ package cdncontainers import ( + "strconv" + "github.com/racker/perigee" "github.com/rackspace/gophercloud" ) @@ -56,3 +58,100 @@ func Enable(c *gophercloud.ServiceClient, containerName string, opts EnableOptsB res.Err = err return res } + +// Get is a function that retrieves the metadata of a container. To extract just +// the custom metadata, pass the GetResult response to the ExtractMetadata +// function. +func Get(c *gophercloud.ServiceClient, containerName string) GetResult { + var res GetResult + resp, err := perigee.Request("HEAD", getURL(c, containerName), perigee.Options{ + MoreHeaders: c.AuthenticatedHeaders(), + OkCodes: []int{200, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} + +// State is the state of an option. It is a pointer to a boolean to enable checking for +// a zero-value of nil instead of false, which is a valid option. +type State *bool + +var ( + iTrue = true + iFalse = false + + // Enabled is used for a true value for options in request bodies. + Enabled State = &iTrue + // Disabled is used for a false value for options in request bodies. + Disabled State = &iFalse +) + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToContainerUpdateMap() (map[string]string, error) +} + +// UpdateOpts is a structure that holds parameters for updating, creating, or +// deleting a container's metadata. +type UpdateOpts struct { + // Whether or not to CDN-enable a container. Prefer using XCDNEnabled, which + // is of type *bool underneath. + // TODO v2.0: change type to Enabled/Disabled (*bool) + CDNEnabled bool `h:"X-Cdn-Enabled"` + // Whether or not to enable log retention. Prefer using XLogRetention, which + // is of type *bool underneath. + // TODO v2.0: change type to Enabled/Disabled (*bool) + LogRetention bool `h:"X-Log-Retention"` + XCDNEnabled *bool + XLogRetention *bool + TTL int `h:"X-Ttl"` +} + +// ToContainerUpdateMap formats a CreateOpts into a map of headers. +func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + h["X-Cdn-Enabled"] = strconv.FormatBool(opts.CDNEnabled) + h["X-Log-Retention"] = strconv.FormatBool(opts.LogRetention) + + if opts.XCDNEnabled != nil { + h["X-Cdn-Enabled"] = strconv.FormatBool(*opts.XCDNEnabled) + } + + if opts.XLogRetention != nil { + h["X-Log-Retention"] = strconv.FormatBool(*opts.XLogRetention) + } + + return h, nil +} + +// Update is a function that creates, updates, or deletes a container's +// metadata. +func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToContainerUpdateMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + resp, err := perigee.Request("POST", updateURL(c, containerName), perigee.Options{ + MoreHeaders: h, + OkCodes: []int{202, 204}, + }) + res.Header = resp.HttpResponse.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go index a5097ca7f65..cb0ad3096c3 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go @@ -1,8 +1,149 @@ package cdncontainers -import "github.com/rackspace/gophercloud" +import ( + "strings" + "time" -// EnableResult represents the result of a get operation. + "github.com/rackspace/gophercloud" +) + +// EnableHeader represents the headers returned in the response from an Enable request. +type EnableHeader struct { + CDNIosURI string `mapstructure:"X-Cdn-Ios-Uri"` + CDNSslURI string `mapstructure:"X-Cdn-Ssl-Uri"` + CDNStreamingURI string `mapstructure:"X-Cdn-Streaming-Uri"` + CDNUri string `mapstructure:"X-Cdn-Uri"` + ContentLength int `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + TransID string `mapstructure:"X-Trans-Id"` +} + +// EnableResult represents the result of an Enable operation. type EnableResult struct { gophercloud.HeaderResult } + +// Extract will return extract an EnableHeader from the response to an Enable +// request. To obtain a map of headers, call the ExtractHeader method on the EnableResult. +func (er EnableResult) Extract() (EnableHeader, error) { + var eh EnableHeader + if er.Err != nil { + return eh, er.Err + } + + if err := gophercloud.DecodeHeader(er.Header, &eh); err != nil { + return eh, err + } + + if date, ok := er.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, er.Header["Date"][0]) + if err != nil { + return eh, err + } + eh.Date = t + } + + return eh, nil +} + +// GetHeader represents the headers returned in the response from a Get request. +type GetHeader struct { + CDNEnabled bool `mapstructure:"X-Cdn-Enabled"` + CDNIosURI string `mapstructure:"X-Cdn-Ios-Uri"` + CDNSslURI string `mapstructure:"X-Cdn-Ssl-Uri"` + CDNStreamingURI string `mapstructure:"X-Cdn-Streaming-Uri"` + CDNUri string `mapstructure:"X-Cdn-Uri"` + ContentLength int `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + LogRetention bool `mapstructure:"X-Log-Retention"` + TransID string `mapstructure:"X-Trans-Id"` + TTL int `mapstructure:"X-Ttl"` +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Get. To obtain +// a map of headers, call the ExtractHeader method on the GetResult. +func (gr GetResult) Extract() (GetHeader, error) { + var gh GetHeader + if gr.Err != nil { + return gh, gr.Err + } + + if err := gophercloud.DecodeHeader(gr.Header, &gh); err != nil { + return gh, err + } + + if date, ok := gr.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, gr.Header["Date"][0]) + if err != nil { + return gh, err + } + gh.Date = t + } + + return gh, nil +} + +// ExtractMetadata is a function that takes a GetResult (of type *http.Response) +// and returns the custom metadata associated with the container. +func (gr GetResult) ExtractMetadata() (map[string]string, error) { + if gr.Err != nil { + return nil, gr.Err + } + metadata := make(map[string]string) + for k, v := range gr.Header { + if strings.HasPrefix(k, "X-Container-Meta-") { + key := strings.TrimPrefix(k, "X-Container-Meta-") + metadata[key] = v[0] + } + } + return metadata, nil +} + +// UpdateHeader represents the headers returned in the response from a Update request. +type UpdateHeader struct { + CDNIosURI string `mapstructure:"X-Cdn-Ios-Uri"` + CDNSslURI string `mapstructure:"X-Cdn-Ssl-Uri"` + CDNStreamingURI string `mapstructure:"X-Cdn-Streaming-Uri"` + CDNUri string `mapstructure:"X-Cdn-Uri"` + ContentLength int `mapstructure:"Content-Length"` + ContentType string `mapstructure:"Content-Type"` + Date time.Time `mapstructure:"-"` + TransID string `mapstructure:"X-Trans-Id"` +} + +// UpdateResult represents the result of an update operation. To extract the +// the headers from the HTTP response, you can invoke the 'ExtractHeader' +// method on the result struct. +type UpdateResult struct { + gophercloud.HeaderResult +} + +// Extract will return a struct of headers returned from a call to Update. To obtain +// a map of headers, call the ExtractHeader method on the UpdateResult. +func (ur UpdateResult) Extract() (UpdateHeader, error) { + var uh UpdateHeader + if ur.Err != nil { + return uh, ur.Err + } + + if err := gophercloud.DecodeHeader(ur.Header, &uh); err != nil { + return uh, err + } + + if date, ok := ur.Header["Date"]; ok && len(date) > 0 { + t, err := time.Parse(time.RFC1123, ur.Header["Date"][0]) + if err != nil { + return uh, err + } + uh.Date = t + } + + return uh, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go index 80653f27624..541249a9273 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go @@ -5,3 +5,11 @@ import "github.com/rackspace/gophercloud" func enableURL(c *gophercloud.ServiceClient, containerName string) string { return c.ServiceURL(containerName) } + +func getURL(c *gophercloud.ServiceClient, container string) string { + return c.ServiceURL(container) +} + +func updateURL(c *gophercloud.ServiceClient, container string) string { + return getURL(c, container) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go index bd4a4f08355..028d66a0ab7 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go @@ -88,3 +88,7 @@ func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts os func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts os.UpdateOptsBuilder) os.UpdateResult { return os.Update(c, containerName, objectName, opts) } + +func CreateTempURL(c *gophercloud.ServiceClient, containerName, objectName string, opts os.CreateTempURLOpts) (string, error) { + return os.CreateTempURL(c, containerName, objectName, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go index 08831ec56a1..8ab8029c37b 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go @@ -66,14 +66,24 @@ func TestListObjectNames(t *testing.T) { func TestCreateObject(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - os.HandleCreateObjectSuccessfully(t) + os.HandleCreateTextObjectSuccessfully(t) content := bytes.NewBufferString("Did gyre and gimble in the wabe") - options := &os.CreateOpts{ContentType: "application/json"} + options := &os.CreateOpts{ContentType: "text/plain"} res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options) th.AssertNoErr(t, res.Err) } +func TestCreateObjectWithoutContentType(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateTypelessObjectSuccessfully(t) + + content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.") + res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &os.CreateOpts{}) + th.AssertNoErr(t, res.Err) +} + func TestCopyObject(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go index f480bc7ca39..7c86ce46230 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go @@ -3,23 +3,42 @@ package gophercloud import ( "encoding/json" "net/http" + "reflect" + + "github.com/mitchellh/mapstructure" ) -// Result acts as a base struct that other results can embed. +/* +Result is an internal type to be used by individual resource packages, but its +methods will be available on a wide variety of user-facing embedding types. + +It acts as a base struct that other Result types, returned from request +functions, can embed for convenience. All Results capture basic information +from the HTTP transaction that was performed, including the response body, +HTTP headers, and any errors that happened. + +Generally, each Result type will have an Extract method that can be used to +further interpret the result's payload in a specific context. Extensions or +providers can then provide additional extraction functions to pull out +provider- or extension-specific information as well. +*/ type Result struct { - // Body is the payload of the HTTP response from the server. In most cases, this will be the - // deserialized JSON structure. + // Body is the payload of the HTTP response from the server. In most cases, + // this will be the deserialized JSON structure. Body interface{} // Header contains the HTTP header structure from the original response. Header http.Header - // Err is an error that occurred during the operation. It's deferred until extraction to make - // it easier to chain operations. + // Err is an error that occurred during the operation. It's deferred until + // extraction to make it easier to chain the Extract call. Err error } -// PrettyPrintJSON creates a string containing the full response body as pretty-printed JSON. +// PrettyPrintJSON creates a string containing the full response body as +// pretty-printed JSON. It's useful for capturing test fixtures and for +// debugging extraction bugs. If you include its output in an issue related to +// a buggy extraction function, we will all love you forever. func (r Result) PrettyPrintJSON() string { pretty, err := json.MarshalIndent(r.Body, "", " ") if err != nil { @@ -28,44 +47,92 @@ func (r Result) PrettyPrintJSON() string { return string(pretty) } -// ErrResult represents results that only contain a potential error and -// nothing else. Usually if the operation executed successfully, the Err field -// will be nil; otherwise it will be stocked with a relevant error. +// ErrResult is an internal type to be used by individual resource packages, but +// its methods will be available on a wide variety of user-facing embedding +// types. +// +// It represents results that only contain a potential error and +// nothing else. Usually, if the operation executed successfully, the Err field +// will be nil; otherwise it will be stocked with a relevant error. Use the +// ExtractErr method +// to cleanly pull it out. type ErrResult struct { Result } -// ExtractErr is a function that extracts error information from a result. +// ExtractErr is a function that extracts error information, or nil, from a result. func (r ErrResult) ExtractErr() error { return r.Err } -// HeaderResult represents a result that only contains an `error` (possibly nil) -// and an http.Header. This is used, for example, by the `objectstorage` packages -// in `openstack`, because most of the operations don't return response bodies. +/* +HeaderResult is an internal type to be used by individual resource packages, but +its methods will be available on a wide variety of user-facing embedding types. + +It represents a result that only contains an error (possibly nil) and an +http.Header. This is used, for example, by the objectstorage packages in +openstack, because most of the operations don't return response bodies, but do +have relevant information in headers. +*/ type HeaderResult struct { Result } // ExtractHeader will return the http.Header and error from the HeaderResult. -// Usage: header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader() +// +// header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader() func (hr HeaderResult) ExtractHeader() (http.Header, error) { return hr.Header, hr.Err } -// RFC3339Milli describes a time format used by API responses. +// DecodeHeader is a function that decodes a header (usually of type map[string]interface{}) to +// another type (usually a struct). This function is used by the objectstorage package to give +// users access to response headers without having to query a map. A DecodeHookFunction is used, +// because OpenStack-based clients return header values as arrays (Go slices). +func DecodeHeader(from, to interface{}) error { + config := &mapstructure.DecoderConfig{ + DecodeHook: func(from, to reflect.Kind, data interface{}) (interface{}, error) { + if from == reflect.Slice { + return data.([]string)[0], nil + } + return data, nil + }, + Result: to, + WeaklyTypedInput: true, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + if err := decoder.Decode(from); err != nil { + return err + } + return nil +} + +// RFC3339Milli describes a common time format used by some API responses. const RFC3339Milli = "2006-01-02T15:04:05.999999Z" -// Link represents a structure that enables paginated collections how to -// traverse backward or forward. The "Rel" field is usually either "next". +/* +Link is an internal type to be used in packages of collection resources that are +paginated in a certain way. + +It's a response substructure common to many paginated collection results that is +used to point to related pages. Usually, the one we care about is the one with +Rel field set to "next". +*/ type Link struct { Href string `mapstructure:"href"` Rel string `mapstructure:"rel"` } -// ExtractNextURL attempts to extract the next URL from a JSON structure. It -// follows the common convention of nesting back and next URLs in a "links" -// JSON array. +/* +ExtractNextURL is an internal function useful for packages of collection +resources that are paginated in a certain way. + +It attempts attempts to extract the "next" URL from slice of Link structs, or +"" if no such URL is present. +*/ func ExtractNextURL(links []Link) (string, error) { var url string diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go index 101fd3953d1..fbd9fe9f381 100644 --- a/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go @@ -7,7 +7,9 @@ import ( ) // WaitFor polls a predicate function, once per second, up to a timeout limit. -// It usually does this to wait for the resource to transition to a certain state. +// It usually does this to wait for a resource to transition to a certain state. +// Resource packages will wrap this in a more convenient function that's +// specific to a certain resource, but it can also be useful on its own. func WaitFor(timeout int, predicate func() (bool, error)) error { start := time.Now().Second() for { @@ -30,7 +32,10 @@ func WaitFor(timeout int, predicate func() (bool, error)) error { } } -// NormalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient. +// NormalizeURL is an internal function to be used by provider clients. +// +// It ensures that each endpoint URL has a closing `/`, as expected by +// ServiceClient's methods. func NormalizeURL(url string) string { if !strings.HasSuffix(url, "/") { return url + "/" diff --git a/cluster/rackspace/cloud-config/master-cloud-config.yaml b/cluster/rackspace/cloud-config/master-cloud-config.yaml index 69f7954f8aa..264b94e3b06 100644 --- a/cluster/rackspace/cloud-config/master-cloud-config.yaml +++ b/cluster/rackspace/cloud-config/master-cloud-config.yaml @@ -1,21 +1,24 @@ #cloud-config write_files: - - path: /opt/bin/regen-minion-list.sh - permissions: 0755 + - path: /etc/cloud.conf + permissions: 0600 content: | - #!/bin/sh - m=$(echo $(etcdctl ls --recursive /corekube/minions | cut -d/ -f4 | sort) | tr ' ' ,) - echo "Found $m" - mkdir -p /run/kube-apiserver - echo "MINIONS=$m" > /run/kube-apiserver/minions.env + [Global] + auth-url = OS_AUTH_URL + username = OS_USERNAME + api-key = OS_PASSWORD + tenant-id = OS_TENANT_NAME + region = OS_REGION_NAME + [LoadBalancer] + subnet-id = 11111111-1111-1111-1111-111111111111 - path: /opt/bin/git-kubernetes-nginx.sh permissions: 0755 content: | #!/bin/bash - git clone https://github.com/doublerr/kubernetes_nginx /opt/kubernetes_nginx + git clone https://github.com/thommay/kubernetes_nginx /opt/kubernetes_nginx /usr/bin/cp /opt/.kubernetes_auth /opt/kubernetes_nginx/.kubernetes_auth - docker build -t kubernetes_nginx:latest /opt/kubernetes_nginx + /opt/kubernetes_nginx/git-kubernetes-nginx.sh - path: /opt/bin/download-release.sh permissions: 0755 content: | @@ -70,40 +73,13 @@ coreos: Documentation=https://github.com/GoogleCloudPlatform/kubernetes After=network-online.target Requires=network-online.target - After=minion-finder.service - Requires=minion-finder.service After=download-release.service Requires=download-release.service [Service] - EnvironmentFile=-/run/kube-apiserver/minions.env ExecStartPre=/usr/bin/ln -sf /opt/kubernetes/server/bin/kube-apiserver /opt/bin/kube-apiserver - ExecStart=/opt/bin/kube-apiserver --address=127.0.0.1 --port=8080 --etcd_servers=http://127.0.0.1:4001 --portal_net=PORTAL_NET --logtostderr=true + ExecStart=/opt/bin/kube-apiserver --address=127.0.0.1 --port=8080 --etcd_servers=http://127.0.0.1:4001 --portal_net=PORTAL_NET --logtostderr=true --cloud_provider=rackspace --cloud_config=/etc/cloud.conf --v=2 Restart=always RestartSec=2 - - name: master-apiserver-sighup.path - command: start - content: | - [Path] - PathChanged=/run/kube-apiserver/minions.env - - name: master-apiserver-sighup.service - command: start - content: | - [Service] - ExecStart=/usr/bin/pkill -SIGHUP -f kube-apiserver - - name: minion-finder.service - command: start - content: | - [Unit] - Description=Kubernetes Minion finder - After=network-online.target - Requires=network-online.target - After=etcd.service - Requires=etcd.service - [Service] - ExecStartPre=/opt/bin/regen-minion-list.sh - ExecStart=/usr/bin/etcdctl exec-watch --recursive /corekube/minions -- /opt/bin/regen-minion-list.sh - Restart=always - RestartSec=30 - name: master-controller-manager.service command: start content: | @@ -116,7 +92,7 @@ coreos: Requires=master-apiserver.service [Service] ExecStartPre=/usr/bin/ln -sf /opt/kubernetes/server/bin/kube-controller-manager /opt/bin/kube-controller-manager - ExecStart=/opt/bin/kube-controller-manager --master=127.0.0.1:8080 --machines=${MINIONS} --logtostderr=true + ExecStart=/opt/bin/kube-controller-manager --master=127.0.0.1:8080 --logtostderr=true --cloud_provider=rackspace --cloud_config=/etc/cloud.conf --v=2 Restart=always RestartSec=2 - name: master-scheduler.service @@ -134,6 +110,22 @@ coreos: ExecStart=/opt/bin/kube-scheduler --master=127.0.0.1:8080 --logtostderr=true Restart=always RestartSec=10 + - name: master-register.service + command: start + content: | + [Unit] + Description=Kubernetes Registration Service + Documentation=https://github.com/kelseyhightower/kube-register + + [Service] + ExecStartPre=/usr/bin/wget -N -P /opt/bin http://storage.googleapis.com/kubernetes/kube-register + ExecStartPre=/usr/bin/chmod +x /opt/bin/kube-register + ExecStart=/opt/bin/kube-register \ + --metadata=kubernetes_role=minion \ + --fleet-endpoint=unix:///var/run/fleet.sock \ + --api-endpoint=http://127.0.0.1:8080 + Restart=always + RestartSec=10 #Running nginx service with --net="host" is a necessary evil until running all k8s services in docker. - name: kubernetes-nginx.service command: start @@ -146,6 +138,7 @@ coreos: Requires=docker.service [Service] ExecStartPre=/opt/bin/git-kubernetes-nginx.sh + ExecStartPre=-/usr/bin/docker rm kubernetes_nginx ExecStart=/usr/bin/docker run --rm --net="host" -p "443:443" -t --name "kubernetes_nginx" kubernetes_nginx ExecStop=/usr/bin/docker stop kubernetes_nginx Restart=always diff --git a/cluster/rackspace/cloud-config/minion-cloud-config.yaml b/cluster/rackspace/cloud-config/minion-cloud-config.yaml index 307722d2ee7..dbae93b38a6 100644 --- a/cluster/rackspace/cloud-config/minion-cloud-config.yaml +++ b/cluster/rackspace/cloud-config/minion-cloud-config.yaml @@ -120,7 +120,7 @@ coreos: Requires=download-release.service [Service] ExecStartPre=/usr/bin/ln -sf /opt/kubernetes/server/bin/kube-proxy /opt/bin/kube-proxy - ExecStart=/opt/bin/kube-proxy --bind_address=$private_ipv4 --etcd_servers=http://127.0.0.1:4001 --logtostderr=true + ExecStart=/opt/bin/kube-proxy --bind_address=$private_ipv4 --etcd_servers=http://127.0.0.1:4001 --logtostderr=true --v=2 Restart=always RestartSec=2 - name: minion-advertiser.service @@ -194,7 +194,7 @@ coreos: [Service] ExecStartPre=/bin/mount --make-rprivate / - ExecStart=/usr/bin/docker -d -s=btrfs -H fd:// -b cbr0 --iptables=false + ExecStart=/usr/bin/docker -d -H fd:// -b cbr0 --iptables=false Restart=always RestartSec=30 diff --git a/cluster/rackspace/util.sh b/cluster/rackspace/util.sh index bc1637d6194..8f67cced9bd 100644 --- a/cluster/rackspace/util.sh +++ b/cluster/rackspace/util.sh @@ -128,7 +128,7 @@ ensure_dev_container() { SWIFTLY_CMD="swiftly -A ${OS_AUTH_URL} -U ${OS_USERNAME} -K ${OS_PASSWORD}" if ! ${SWIFTLY_CMD} get ${CLOUDFILES_CONTAINER} > /dev/null 2>&1 ; then - echo "cluster/rackspace/util.sh: Container doesn't exist. Creating container ${KUBE_RACKSPACE_RELEASE_BUCKET}" + echo "cluster/rackspace/util.sh: Container doesn't exist. Creating container ${CLOUDFILES_CONTAINER}" ${SWIFTLY_CMD} put ${CLOUDFILES_CONTAINER} > /dev/null 2>&1 fi } @@ -155,6 +155,11 @@ rax-boot-master() { -e "s|KUBE_USER|${KUBE_USER}|" \ -e "s|KUBE_PASSWORD|${KUBE_PASSWORD}|" \ -e "s|PORTAL_NET|${PORTAL_NET}|" \ + -e "s|OS_AUTH_URL|${OS_AUTH_URL}|" \ + -e "s|OS_USERNAME|${OS_USERNAME}|" \ + -e "s|OS_PASSWORD|${OS_PASSWORD}|" \ + -e "s|OS_TENANT_NAME|${OS_TENANT_NAME}|" \ + -e "s|OS_REGION_NAME|${OS_REGION_NAME}|" \ $(dirname $0)/rackspace/cloud-config/master-cloud-config.yaml > $KUBE_TEMP/master-cloud-config.yaml diff --git a/pkg/cloudprovider/rackspace/MAINTAINERS.md b/pkg/cloudprovider/rackspace/MAINTAINERS.md new file mode 100644 index 00000000000..6b2c98c7bd2 --- /dev/null +++ b/pkg/cloudprovider/rackspace/MAINTAINERS.md @@ -0,0 +1,3 @@ +# Maintainers + +* [Thom May](https://github.com/thommay) diff --git a/pkg/cloudprovider/rackspace/rackspace.go b/pkg/cloudprovider/rackspace/rackspace.go new file mode 100644 index 00000000000..5ab564c9ee2 --- /dev/null +++ b/pkg/cloudprovider/rackspace/rackspace.go @@ -0,0 +1,409 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rackspace + +import ( + "errors" + "fmt" + "io" + "net" + "regexp" + "time" + + "code.google.com/p/gcfg" + "github.com/rackspace/gophercloud" + os_servers "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace" + "github.com/rackspace/gophercloud/rackspace/compute/v2/flavors" + "github.com/rackspace/gophercloud/rackspace/compute/v2/servers" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" + "github.com/golang/glog" +) + +var ErrNotFound = errors.New("Failed to find object") +var ErrMultipleResults = errors.New("Multiple results where only one expected") +var ErrNoAddressFound = errors.New("No address found for host") +var ErrAttrNotFound = errors.New("Expected attribute not found") + +// encoding.TextUnmarshaler interface for time.Duration +type MyDuration struct { + time.Duration +} + +func (d *MyDuration) UnmarshalText(text []byte) error { + res, err := time.ParseDuration(string(text)) + if err != nil { + return err + } + d.Duration = res + return nil +} + +type LoadBalancerOpts struct { + SubnetId string `gcfg:"subnet-id"` // required + CreateMonitor bool `gcfg:"create-monitor"` + MonitorDelay MyDuration `gcfg:"monitor-delay"` + MonitorTimeout MyDuration `gcfg:"monitor-timeout"` + MonitorMaxRetries uint `gcfg:"monitor-max-retries"` +} + +// Rackspace is an implementation of cloud provider Interface for Rackspace. +type Rackspace struct { + provider *gophercloud.ProviderClient + region string + lbOpts LoadBalancerOpts +} + +type Config struct { + Global struct { + AuthUrl string `gcfg:"auth-url"` + Username string + UserId string `gcfg:"user-id"` + Password string + ApiKey string `gcfg:"api-key"` + TenantId string `gcfg:"tenant-id"` + TenantName string `gcfg:"tenant-name"` + DomainId string `gcfg:"domain-id"` + DomainName string `gcfg:"domain-name"` + Region string + } + LoadBalancer LoadBalancerOpts +} + +func init() { + cloudprovider.RegisterCloudProvider("rackspace", func(config io.Reader) (cloudprovider.Interface, error) { + cfg, err := readConfig(config) + if err != nil { + return nil, err + } + return newRackspace(cfg) + }) +} + +func (cfg Config) toAuthOptions() gophercloud.AuthOptions { + return gophercloud.AuthOptions{ + IdentityEndpoint: cfg.Global.AuthUrl, + Username: cfg.Global.Username, + UserID: cfg.Global.UserId, + Password: cfg.Global.Password, + APIKey: cfg.Global.ApiKey, + TenantID: cfg.Global.TenantId, + TenantName: cfg.Global.TenantName, + + // Persistent service, so we need to be able to renew tokens + AllowReauth: true, + } +} + +func readConfig(config io.Reader) (Config, error) { + if config == nil { + err := fmt.Errorf("no Rackspace cloud provider config file given") + return Config{}, err + } + + var cfg Config + err := gcfg.ReadInto(&cfg, config) + return cfg, err +} + +func newRackspace(cfg Config) (*Rackspace, error) { + provider, err := rackspace.AuthenticatedClient(cfg.toAuthOptions()) + if err != nil { + return nil, err + } + + os := Rackspace{ + provider: provider, + region: cfg.Global.Region, + lbOpts: cfg.LoadBalancer, + } + return &os, nil +} + +type Instances struct { + compute *gophercloud.ServiceClient + flavor_to_resource map[string]*api.NodeResources // keyed by flavor id +} + +// Instances returns an implementation of Instances for Rackspace. +func (os *Rackspace) Instances() (cloudprovider.Instances, bool) { + glog.V(2).Info("rackspace.Instances() called") + + compute, err := rackspace.NewComputeV2(os.provider, gophercloud.EndpointOpts{ + Region: os.region, + }) + if err != nil { + glog.Warningf("Failed to find compute endpoint: %v", err) + return nil, false + } + + pager := flavors.ListDetail(compute, nil) + + flavor_to_resource := make(map[string]*api.NodeResources) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + flavorList, err := flavors.ExtractFlavors(page) + if err != nil { + return false, err + } + for _, flavor := range flavorList { + rsrc := api.NodeResources{ + Capacity: api.ResourceList{ + api.ResourceCPU: *resource.NewMilliQuantity(int64(flavor.VCPUs*1000), resource.DecimalSI), + api.ResourceMemory: resource.MustParse(fmt.Sprintf("%dMi", flavor.RAM)), + "openstack.org/disk": resource.MustParse(fmt.Sprintf("%dG", flavor.Disk)), + "openstack.org/rxTxFactor": *resource.NewQuantity(int64(flavor.RxTxFactor*1000), resource.DecimalSI), + "openstack.org/swap": resource.MustParse(fmt.Sprintf("%dMi", flavor.Swap)), + }, + } + flavor_to_resource[flavor.ID] = &rsrc + } + return true, nil + }) + if err != nil { + glog.Warningf("Failed to find compute flavors: %v", err) + return nil, false + } + + glog.V(2).Infof("Found %v compute flavors", len(flavor_to_resource)) + glog.V(1).Info("Claiming to support Instances") + + return &Instances{compute, flavor_to_resource}, true +} + +func (i *Instances) List(name_filter string) ([]string, error) { + glog.V(2).Infof("rackspace List(%v) called", name_filter) + + opts := os_servers.ListOpts{ + Name: name_filter, + Status: "ACTIVE", + } + pager := servers.List(i.compute, opts) + + ret := make([]string, 0) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + sList, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + for _, server := range sList { + ret = append(ret, server.Name) + } + return true, nil + }) + if err != nil { + return nil, err + } + + glog.V(2).Infof("Found %v entries: %v", len(ret), ret) + + return ret, nil +} + +func serverHasAddress(srv os_servers.Server, ip string) bool { + if ip == firstAddr(srv.Addresses["private"]) { + return true + } + if ip == firstAddr(srv.Addresses["public"]) { + return true + } + if ip == srv.AccessIPv4 { + return true + } + if ip == srv.AccessIPv6 { + return true + } + return false +} + +func getServerByAddress(client *gophercloud.ServiceClient, name string) (*os_servers.Server, error) { + pager := servers.List(client, nil) + + serverList := make([]os_servers.Server, 0, 1) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + s, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + for _, v := range s { + if serverHasAddress(v, name) { + serverList = append(serverList, v) + } + } + if len(serverList) > 1 { + return false, ErrMultipleResults + } + return true, nil + }) + if err != nil { + return nil, err + } + + if len(serverList) == 0 { + return nil, ErrNotFound + } else if len(serverList) > 1 { + return nil, ErrMultipleResults + } + + return &serverList[0], nil +} + +func getServerByName(client *gophercloud.ServiceClient, name string) (*os_servers.Server, error) { + if net.ParseIP(name) != nil { + // we're an IP, so we'll have to walk the full list of servers to + // figure out which one we are. + return getServerByAddress(client, name) + } + opts := os_servers.ListOpts{ + Name: fmt.Sprintf("^%s$", regexp.QuoteMeta(name)), + Status: "ACTIVE", + } + pager := servers.List(client, opts) + + serverList := make([]os_servers.Server, 0, 1) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + s, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + serverList = append(serverList, s...) + if len(serverList) > 1 { + return false, ErrMultipleResults + } + return true, nil + }) + if err != nil { + return nil, err + } + + if len(serverList) == 0 { + return nil, ErrNotFound + } else if len(serverList) > 1 { + return nil, ErrMultipleResults + } + + return &serverList[0], nil +} + +func firstAddr(netblob interface{}) string { + // Run-time types for the win :( + list, ok := netblob.([]interface{}) + if !ok || len(list) < 1 { + return "" + } + props, ok := list[0].(map[string]interface{}) + if !ok { + return "" + } + tmp, ok := props["addr"] + if !ok { + return "" + } + addr, ok := tmp.(string) + if !ok { + return "" + } + return addr +} + +func getAddressByName(api *gophercloud.ServiceClient, name string) (string, error) { + srv, err := getServerByName(api, name) + if err != nil { + return "", err + } + + var s string + if s == "" { + s = firstAddr(srv.Addresses["private"]) + } + if s == "" { + s = firstAddr(srv.Addresses["public"]) + } + if s == "" { + s = srv.AccessIPv4 + } + if s == "" { + s = srv.AccessIPv6 + } + if s == "" { + return "", ErrNoAddressFound + } + return s, nil +} + +func (i *Instances) IPAddress(name string) (net.IP, error) { + glog.V(2).Infof("IPAddress(%v) called", name) + + ip, err := getAddressByName(i.compute, name) + if err != nil { + return nil, err + } + + glog.V(2).Infof("IPAddress(%v) => %v", name, ip) + + return net.ParseIP(ip), err +} + +func (i *Instances) GetNodeResources(name string) (*api.NodeResources, error) { + glog.V(2).Infof("GetNodeResources(%v) called", name) + + srv, err := getServerByName(i.compute, name) + if err != nil { + return nil, err + } + + s, ok := srv.Flavor["id"] + if !ok { + return nil, ErrAttrNotFound + } + flavId, ok := s.(string) + if !ok { + return nil, ErrAttrNotFound + } + rsrc, ok := i.flavor_to_resource[flavId] + if !ok { + return nil, ErrNotFound + } + + glog.V(2).Infof("GetNodeResources(%v) => %v", name, rsrc) + + return rsrc, nil +} + +func (os *Rackspace) Clusters() (cloudprovider.Clusters, bool) { + return nil, false +} + +func (os *Rackspace) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) { + return nil, false +} + +func (os *Rackspace) Zones() (cloudprovider.Zones, bool) { + glog.V(1).Info("Claiming to support Zones") + + return os, true +} +func (os *Rackspace) GetZone() (cloudprovider.Zone, error) { + glog.V(1).Infof("Current zone is %v", os.region) + + return cloudprovider.Zone{Region: os.region}, nil +} diff --git a/pkg/cloudprovider/rackspace/rackspace_test.go b/pkg/cloudprovider/rackspace/rackspace_test.go new file mode 100644 index 00000000000..f72d7cfb449 --- /dev/null +++ b/pkg/cloudprovider/rackspace/rackspace_test.go @@ -0,0 +1,181 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rackspace + +import ( + "os" + "strings" + "testing" + "time" + + "github.com/rackspace/gophercloud" +) + +func TestReadConfig(t *testing.T) { + _, err := readConfig(nil) + if err == nil { + t.Errorf("Should fail when no config is provided: %s", err) + } + + cfg, err := readConfig(strings.NewReader(` +[Global] +auth-url = http://auth.url +username = user +[LoadBalancer] +create-monitor = yes +monitor-delay = 1m +monitor-timeout = 30s +monitor-max-retries = 3 +`)) + if err != nil { + t.Fatalf("Should succeed when a valid config is provided: %s", err) + } + if cfg.Global.AuthUrl != "http://auth.url" { + t.Errorf("incorrect authurl: %s", cfg.Global.AuthUrl) + } + + if !cfg.LoadBalancer.CreateMonitor { + t.Errorf("incorrect lb.createmonitor: %t", cfg.LoadBalancer.CreateMonitor) + } + if cfg.LoadBalancer.MonitorDelay.Duration != 1*time.Minute { + t.Errorf("incorrect lb.monitordelay: %s", cfg.LoadBalancer.MonitorDelay) + } + if cfg.LoadBalancer.MonitorTimeout.Duration != 30*time.Second { + t.Errorf("incorrect lb.monitortimeout: %s", cfg.LoadBalancer.MonitorTimeout) + } + if cfg.LoadBalancer.MonitorMaxRetries != 3 { + t.Errorf("incorrect lb.monitormaxretries: %d", cfg.LoadBalancer.MonitorMaxRetries) + } +} + +func TestToAuthOptions(t *testing.T) { + cfg := Config{} + cfg.Global.Username = "user" + // etc. + + ao := cfg.toAuthOptions() + + if !ao.AllowReauth { + t.Errorf("Will need to be able to reauthenticate") + } + if ao.Username != cfg.Global.Username { + t.Errorf("Username %s != %s", ao.Username, cfg.Global.Username) + } +} + +// This allows acceptance testing against an existing Rackspace +// install, using the standard OS_* Rackspace client environment +// variables. +// FIXME: it would be better to hermetically test against canned JSON +// requests/responses. +func configFromEnv() (cfg Config, ok bool) { + cfg.Global.AuthUrl = os.Getenv("OS_AUTH_URL") + + cfg.Global.TenantId = os.Getenv("OS_TENANT_ID") + // Rax/nova _insists_ that we don't specify both tenant ID and name + if cfg.Global.TenantId == "" { + cfg.Global.TenantName = os.Getenv("OS_TENANT_NAME") + } + + cfg.Global.Username = os.Getenv("OS_USERNAME") + cfg.Global.Password = os.Getenv("OS_PASSWORD") + cfg.Global.ApiKey = os.Getenv("OS_API_KEY") + cfg.Global.Region = os.Getenv("OS_REGION_NAME") + cfg.Global.DomainId = os.Getenv("OS_DOMAIN_ID") + cfg.Global.DomainName = os.Getenv("OS_DOMAIN_NAME") + + ok = (cfg.Global.AuthUrl != "" && + cfg.Global.Username != "" && + (cfg.Global.Password != "" || cfg.Global.ApiKey != "") && + (cfg.Global.TenantId != "" || cfg.Global.TenantName != "" || + cfg.Global.DomainId != "" || cfg.Global.DomainName != "")) + + return +} + +func TestNewRackspace(t *testing.T) { + cfg, ok := configFromEnv() + if !ok { + t.Skipf("No config found in environment") + } + + _, err := newRackspace(cfg) + if err != nil { + t.Fatalf("Failed to construct/authenticate Rackspace: %s", err) + } +} + +func TestInstances(t *testing.T) { + cfg, ok := configFromEnv() + if !ok { + t.Skipf("No config found in environment") + } + + os, err := newRackspace(cfg) + if err != nil { + t.Fatalf("Failed to construct/authenticate Rackspace: %s", err) + } + + i, ok := os.Instances() + if !ok { + t.Fatalf("Instances() returned false") + } + + srvs, err := i.List(".") + if err != nil { + t.Fatalf("Instances.List() failed: %s", err) + } + if len(srvs) == 0 { + t.Fatalf("Instances.List() returned zero servers") + } + t.Logf("Found servers (%d): %s\n", len(srvs), srvs) + + ip, err := i.IPAddress(srvs[0]) + if err != nil { + t.Fatalf("Instances.IPAddress(%s) failed: %s", srvs[0], err) + } + t.Logf("Found IPAddress(%s) = %s\n", srvs[0], ip) + + rsrcs, err := i.GetNodeResources(srvs[0]) + if err != nil { + t.Fatalf("Instances.GetNodeResources(%s) failed: %s", srvs[0], err) + } + t.Logf("Found GetNodeResources(%s) = %s\n", srvs[0], rsrcs) +} + +func TestZones(t *testing.T) { + os := Rackspace{ + provider: &gophercloud.ProviderClient{ + IdentityBase: "http://auth.url/", + }, + region: "myRegion", + } + + z, ok := os.Zones() + if !ok { + t.Fatalf("Zones() returned false") + } + + zone, err := z.GetZone() + if err != nil { + t.Fatalf("GetZone() returned error: %s", err) + } + + if zone.Region != "myRegion" { + t.Fatalf("GetZone() returned wrong region (%s)", zone.Region) + } +} diff --git a/pkg/controllermanager/plugins.go b/pkg/controllermanager/plugins.go index f58afbdc4c8..391b74c7509 100644 --- a/pkg/controllermanager/plugins.go +++ b/pkg/controllermanager/plugins.go @@ -24,5 +24,6 @@ import ( _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt" + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/rackspace" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant" ) diff --git a/pkg/master/server/plugins.go b/pkg/master/server/plugins.go index 9f3039b7c0c..70671b8c69a 100644 --- a/pkg/master/server/plugins.go +++ b/pkg/master/server/plugins.go @@ -25,6 +25,7 @@ import ( _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt" + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/rackspace" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant" // Admission policies