diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index d681e76f0bc1..49657130e88b 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -54,6 +54,26 @@ "ImportPath": "github.com/AdRoll/goamz/s3", "Rev": "c73835dc8fc6958baf8df8656864ee4d6d04b130" }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/cmd/kube-apiserver/app", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/cmd/kube-controller-manager/app", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/cmd/kube-proxy/app", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/cmd/kubelet/app", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/admission", "Comment": "v0.17.0-144-g25d32ee", @@ -329,21 +349,41 @@ "Comment": "v0.17.0-144-g25d32ee", "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/watch", "Comment": "v0.17.0-144-g25d32ee", "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/cmd/kube-scheduler/app", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit", "Comment": "v0.17.0-144-g25d32ee", "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger", "Comment": "v0.17.0-144-g25d32ee", "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/autoprovision", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists", "Comment": "v0.17.0-144-g25d32ee", @@ -359,6 +399,16 @@ "Comment": "v0.17.0-144-g25d32ee", "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile", "Comment": "v0.17.0-144-g25d32ee", @@ -389,6 +439,11 @@ "Comment": "v0.17.0-144-g25d32ee", "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/third_party/forked/coreos/go-etcd/etcd", + "Comment": "v0.17.0-144-g25d32ee", + "Rev": "25d32ee5132b41c122fe2929f3c6be7c3eb74f1d" + }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/third_party/forked/json", "Comment": "v0.17.0-144-g25d32ee", @@ -904,6 +959,31 @@ "ImportPath": "github.com/miekg/dns", "Rev": "c13058f493c3756207ced654dce2986e812f2bcf" }, + { + "ImportPath": "github.com/mitchellh/goamz/aws", + "Rev": "703cfb45985762869e465f37ed030ff01615ff1e" + }, + { + "ImportPath": "github.com/mitchellh/goamz/ec2", + "Rev": "703cfb45985762869e465f37ed030ff01615ff1e" + }, + { + "ImportPath": "github.com/mitchellh/mapstructure", + "Rev": "740c764bc6149d3f1806231418adb9f52c11bcbf" + }, + { + "ImportPath": "github.com/openshift/openshift-sdn/ovssubnet", + "Rev": "3ba813b579ee95a86599ba0896f2d470d7702a19" + }, + { + "ImportPath": "github.com/openshift/openshift-sdn/pkg/api", + "Rev": "3ba813b579ee95a86599ba0896f2d470d7702a19" + }, + { + "ImportPath": "github.com/openshift/openshift-sdn/pkg/netutils", + "Comment": "v0.1-83-g3ba813b", + "Rev": "3ba813b579ee95a86599ba0896f2d470d7702a19" + }, { "ImportPath": "github.com/openshift/source-to-image/pkg/api", "Comment": "v0.5", @@ -972,6 +1052,11 @@ "ImportPath": "github.com/prometheus/procfs", "Rev": "490cc6eb5fa45bf8a8b7b73c8bc82a8160e8531d" }, + { + "ImportPath": "github.com/rackspace/gophercloud", + "Comment": "v1.0.0-569-gf3ced00", + "Rev": "f3ced00552c1c7d4a6184500af9062cfb4ff4463" + }, { "ImportPath": "github.com/skynetservices/skydns/backends/etcd", "Comment": "2.0.1d-99-gd442ef6", @@ -1016,6 +1101,10 @@ "ImportPath": "github.com/syndtr/gocapability/capability", "Rev": "8e4cdcb3c22b40d5e330ade0b68cb2e2a3cf6f98" }, + { + "ImportPath": "github.com/vaughan0/go-ini", + "Rev": "a98ad7ee00ec53921f08832bc06ecf7fd600e6a1" + }, { "ImportPath": "golang.org/x/net/context", "Rev": "cbcac7bb8415db9b6cb4d1ebab1dc9afbd688b97" @@ -1055,14 +1144,6 @@ { "ImportPath": "speter.net/go/exp/math/dec/inf", "Rev": "42ca6cd68aa922bc3f32f1e056e61b65945d9ad7" - }, - { - "ImportPath": "github.com/openshift/openshift-sdn/ovssubnet", - "Rev": "3ba813b579ee95a86599ba0896f2d470d7702a19" - }, - { - "ImportPath": "github.com/openshift/openshift-sdn/pkg/api", - "Rev": "3ba813b579ee95a86599ba0896f2d470d7702a19" } ] } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-apiserver/app/plugins.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-apiserver/app/plugins.go new file mode 100644 index 000000000000..fe2dca4cea76 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-apiserver/app/plugins.go @@ -0,0 +1,41 @@ +/* +Copyright 2014 The Kubernetes Authors 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 app + +// This file exists to force the desired plugin implementations to be linked. +// This should probably be part of some configuration fed into the build for a +// given binary target. +import ( + // Cloud providers + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws" + _ "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 + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/autoprovision" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/lifecycle" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount" +) diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-apiserver/app/server.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-apiserver/app/server.go new file mode 100644 index 000000000000..26fc6ac46d94 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-apiserver/app/server.go @@ -0,0 +1,447 @@ +/* +Copyright 2014 The Kubernetes Authors 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 app does all of the work necessary to create a Kubernetes +// APIServer by binding together the API, master and APIServer infrastructure. +// It can be configured and called directly or via the hyperkube framework. +package app + +import ( + "crypto/tls" + "net" + "net/http" + "os" + "path" + "regexp" + "strconv" + "strings" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" + "github.com/GoogleCloudPlatform/kubernetes/pkg/master" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + forked "github.com/GoogleCloudPlatform/kubernetes/third_party/forked/coreos/go-etcd/etcd" + + "github.com/coreos/go-etcd/etcd" + "github.com/golang/glog" + "github.com/spf13/pflag" +) + +// APIServer runs a kubernetes api server. +type APIServer struct { + InsecureBindAddress util.IP + InsecurePort int + BindAddress util.IP + ReadOnlyPort int + SecurePort int + ExternalHost string + APIRate float32 + APIBurst int + TLSCertFile string + TLSPrivateKeyFile string + CertDirectory string + APIPrefix string + StorageVersion string + CloudProvider string + CloudConfigFile string + EventTTL time.Duration + BasicAuthFile string + ClientCAFile string + TokenAuthFile string + ServiceAccountKeyFile string + ServiceAccountLookup bool + AuthorizationMode string + AuthorizationPolicyFile string + AdmissionControl string + AdmissionControlConfigFile string + EtcdServerList util.StringList + EtcdConfigFile string + EtcdPathPrefix string + OldEtcdPathPrefix string + CorsAllowedOriginList util.StringList + AllowPrivileged bool + PortalNet util.IPNet // TODO: make this a list + EnableLogsSupport bool + MasterServiceNamespace string + RuntimeConfig util.ConfigurationMap + KubeletConfig client.KubeletConfig + ClusterName string + EnableProfiling bool + MaxRequestsInFlight int + LongRunningRequestRE string +} + +// NewAPIServer creates a new APIServer object with default parameters +func NewAPIServer() *APIServer { + s := APIServer{ + InsecurePort: 8080, + InsecureBindAddress: util.IP(net.ParseIP("127.0.0.1")), + BindAddress: util.IP(net.ParseIP("0.0.0.0")), + ReadOnlyPort: 7080, + SecurePort: 6443, + APIRate: 10.0, + APIBurst: 200, + APIPrefix: "/api", + EventTTL: 1 * time.Hour, + AuthorizationMode: "AlwaysAllow", + AdmissionControl: "AlwaysAdmit", + EtcdPathPrefix: master.DefaultEtcdPathPrefix, + OldEtcdPathPrefix: master.DefaultEtcdPathPrefix, + EnableLogsSupport: true, + MasterServiceNamespace: api.NamespaceDefault, + ClusterName: "kubernetes", + CertDirectory: "/var/run/kubernetes", + + RuntimeConfig: make(util.ConfigurationMap), + KubeletConfig: client.KubeletConfig{ + Port: 10250, + EnableHttps: true, + HTTPTimeout: time.Duration(5) * time.Second, + }, + } + + return &s +} + +// AddFlags adds flags for a specific APIServer to the specified FlagSet +func (s *APIServer) AddFlags(fs *pflag.FlagSet) { + // Note: the weird ""+ in below lines seems to be the only way to get gofmt to + // arrange these text blocks sensibly. Grrr. + fs.IntVar(&s.InsecurePort, "insecure-port", s.InsecurePort, ""+ + "The port on which to serve unsecured, unauthenticated access. Default 8080. It is assumed "+ + "that firewall rules are set up such that this port is not reachable from outside of "+ + "the cluster and that port 443 on the cluster's public address is proxied to this "+ + "port. This is performed by nginx in the default setup.") + fs.IntVar(&s.InsecurePort, "port", s.InsecurePort, "DEPRECATED: see --insecure-port instead") + fs.Var(&s.InsecureBindAddress, "insecure-bind-address", ""+ + "The IP address on which to serve the --insecure-port (set to 0.0.0.0 for all interfaces). "+ + "Defaults to localhost.") + fs.Var(&s.InsecureBindAddress, "address", "DEPRECATED: see --insecure-bind-address instead") + fs.Var(&s.BindAddress, "bind-address", ""+ + "The IP address on which to serve the --read-only-port and --secure-port ports. This "+ + "address must be reachable by the rest of the cluster. If blank, all interfaces will be used.") + fs.Var(&s.BindAddress, "public-address-override", "DEPRECATED: see --bind-address instead") + fs.IntVar(&s.ReadOnlyPort, "read-only-port", s.ReadOnlyPort, ""+ + "The port on which to serve read-only resources. If 0, don't serve read-only "+ + "at all. It is assumed that firewall rules are set up such that this port is "+ + "not reachable from outside of the cluster.") + fs.IntVar(&s.SecurePort, "secure-port", s.SecurePort, ""+ + "The port on which to serve HTTPS with authentication and authorization. If 0, "+ + "don't serve HTTPS at all.") + fs.Float32Var(&s.APIRate, "api-rate", s.APIRate, "API rate limit as QPS for the read only port") + fs.IntVar(&s.APIBurst, "api-burst", s.APIBurst, "API burst amount for the read only port") + fs.StringVar(&s.TLSCertFile, "tls-cert-file", s.TLSCertFile, ""+ + "File containing x509 Certificate for HTTPS. (CA cert, if any, concatenated after server cert). "+ + "If HTTPS serving is enabled, and --tls-cert-file and --tls-private-key-file are not provided, "+ + "a self-signed certificate and key are generated for the public address and saved to /var/run/kubernetes.") + fs.StringVar(&s.TLSPrivateKeyFile, "tls-private-key-file", s.TLSPrivateKeyFile, "File containing x509 private key matching --tls-cert-file.") + fs.StringVar(&s.CertDirectory, "cert-dir", s.CertDirectory, "The directory where the TLS certs are located (by default /var/run/kubernetes). "+ + "If --tls-cert-file and --tls-private-key-file are provided, this flag will be ignored.") + fs.StringVar(&s.APIPrefix, "api-prefix", s.APIPrefix, "The prefix for API requests on the server. Default '/api'.") + fs.StringVar(&s.StorageVersion, "storage-version", s.StorageVersion, "The version to store resources with. Defaults to server preferred") + fs.StringVar(&s.CloudProvider, "cloud-provider", s.CloudProvider, "The provider for cloud services. Empty string for no provider.") + fs.StringVar(&s.CloudConfigFile, "cloud-config", s.CloudConfigFile, "The path to the cloud provider configuration file. Empty string for no configuration file.") + fs.DurationVar(&s.EventTTL, "event-ttl", s.EventTTL, "Amount of time to retain events. Default 1 hour.") + fs.StringVar(&s.BasicAuthFile, "basic-auth-file", s.BasicAuthFile, "If set, the file that will be used to admit requests to the secure port of the API server via http basic authentication.") + fs.StringVar(&s.ClientCAFile, "client-ca-file", s.ClientCAFile, "If set, any request presenting a client certificate signed by one of the authorities in the client-ca-file is authenticated with an identity corresponding to the CommonName of the client certificate.") + fs.StringVar(&s.TokenAuthFile, "token-auth-file", s.TokenAuthFile, "If set, the file that will be used to secure the secure port of the API server via token authentication.") + fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, "File containing PEM-encoded x509 RSA private or public key, used to verify ServiceAccount tokens. If unspecified, --tls-private-key-file is used.") + fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.") + fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ",")) + fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.") + fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", ")) + fs.StringVar(&s.AdmissionControlConfigFile, "admission-control-config-file", s.AdmissionControlConfigFile, "File with admission control configuration.") + fs.Var(&s.EtcdServerList, "etcd-servers", "List of etcd servers to watch (http://ip:port), comma separated. Mutually exclusive with -etcd-config") + fs.StringVar(&s.EtcdConfigFile, "etcd-config", s.EtcdConfigFile, "The config file for the etcd client. Mutually exclusive with -etcd-servers.") + fs.StringVar(&s.EtcdPathPrefix, "etcd-prefix", s.EtcdPathPrefix, "The prefix for all resource paths in etcd.") + fs.StringVar(&s.OldEtcdPathPrefix, "old-etcd-prefix", s.OldEtcdPathPrefix, "The previous prefix for all resource paths in etcd, if any.") + fs.Var(&s.CorsAllowedOriginList, "cors-allowed-origins", "List of allowed origins for CORS, comma separated. An allowed origin can be a regular expression to support subdomain matching. If this list is empty CORS will not be enabled.") + fs.BoolVar(&s.AllowPrivileged, "allow-privileged", s.AllowPrivileged, "If true, allow privileged containers.") + fs.Var(&s.PortalNet, "portal-net", "A CIDR notation IP range from which to assign portal IPs. This must not overlap with any IP ranges assigned to nodes for pods.") + fs.StringVar(&s.MasterServiceNamespace, "master-service-namespace", s.MasterServiceNamespace, "The namespace from which the kubernetes master services should be injected into pods") + fs.Var(&s.RuntimeConfig, "runtime-config", "A set of key=value pairs that describe runtime configuration that may be passed to the apiserver.") + client.BindKubeletClientConfigFlags(fs, &s.KubeletConfig) + fs.StringVar(&s.ClusterName, "cluster-name", s.ClusterName, "The instance prefix for the cluster") + fs.BoolVar(&s.EnableProfiling, "profiling", true, "Enable profiling via web interface host:port/debug/pprof/") + fs.StringVar(&s.ExternalHost, "external-hostname", "", "The hostname to use when generating externalized URLs for this master (e.g. Swagger API Docs.)") + fs.IntVar(&s.MaxRequestsInFlight, "max-requests-inflight", 400, "The maximum number of requests in flight at a given time. When the server exceeds this, it rejects requests. Zero for no limit.") + fs.StringVar(&s.LongRunningRequestRE, "long-running-request-regexp", "[.*\\/watch$][^\\/proxy.*]", "A regular expression matching long running requests which should be excluded from maximum inflight request handling.") +} + +// TODO: Longer term we should read this from some config store, rather than a flag. +func (s *APIServer) verifyPortalFlags() { + if s.PortalNet.IP == nil { + glog.Fatal("No --portal-net specified") + } +} + +func newEtcd(etcdConfigFile string, etcdServerList util.StringList, storageVersion string, pathPrefix string) (helper tools.EtcdHelper, err error) { + var client tools.EtcdGetSet + if etcdConfigFile != "" { + client, err = etcd.NewClientFromFile(etcdConfigFile) + if err != nil { + return helper, err + } + } else { + etcdClient := etcd.NewClient(etcdServerList) + transport := &http.Transport{ + Dial: forked.Dial, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + MaxIdleConnsPerHost: 500, + } + etcdClient.SetTransport(transport) + client = etcdClient + } + + return master.NewEtcdHelper(client, storageVersion, pathPrefix) +} + +// Run runs the specified APIServer. This should never exit. +func (s *APIServer) Run(_ []string) error { + s.verifyPortalFlags() + + if (s.EtcdConfigFile != "" && len(s.EtcdServerList) != 0) || (s.EtcdConfigFile == "" && len(s.EtcdServerList) == 0) { + glog.Fatalf("specify either --etcd-servers or --etcd-config") + } + + capabilities.Initialize(capabilities.Capabilities{ + AllowPrivileged: s.AllowPrivileged, + // TODO(vmarmol): Implement support for HostNetworkSources. + HostNetworkSources: []string{}, + }) + + cloud := cloudprovider.InitCloudProvider(s.CloudProvider, s.CloudConfigFile) + + kubeletClient, err := client.NewKubeletClient(&s.KubeletConfig) + if err != nil { + glog.Fatalf("Failure to start kubelet client: %v", err) + } + + disableV1beta3 := false + v1beta3FlagValue, ok := s.RuntimeConfig["api/v1beta3"] + if ok && v1beta3FlagValue == "false" { + disableV1beta3 = true + } + + _, enableV1 := s.RuntimeConfig["api/v1"] + + disableLegacyAPIs := false + legacyAPIFlagValue, ok := s.RuntimeConfig["api/legacy"] + if ok && legacyAPIFlagValue == "false" { + disableLegacyAPIs = true + } + // TODO: expose same flags as client.BindClientConfigFlags but for a server + clientConfig := &client.Config{ + Host: net.JoinHostPort(s.InsecureBindAddress.String(), strconv.Itoa(s.InsecurePort)), + Version: s.StorageVersion, + } + client, err := client.New(clientConfig) + if err != nil { + glog.Fatalf("Invalid server address: %v", err) + } + + helper, err := newEtcd(s.EtcdConfigFile, s.EtcdServerList, s.StorageVersion, s.EtcdPathPrefix) + if err != nil { + glog.Fatalf("Invalid storage version or misconfigured etcd: %v", err) + } + + // TODO Is this the right place for migration to happen? Must *both* old and + // new etcd prefix params be supplied for this to be valid? + if s.OldEtcdPathPrefix != "" { + if err = helper.MigrateKeys(s.OldEtcdPathPrefix); err != nil { + glog.Fatalf("Migration of old etcd keys failed: %v", err) + } + } + + n := net.IPNet(s.PortalNet) + + // Default to the private server key for service account token signing + if s.ServiceAccountKeyFile == "" && s.TLSPrivateKeyFile != "" { + s.ServiceAccountKeyFile = s.TLSPrivateKeyFile + } + authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile, s.ServiceAccountKeyFile, s.ServiceAccountLookup, client) + if err != nil { + glog.Fatalf("Invalid Authentication Config: %v", err) + } + + authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(s.AuthorizationMode, s.AuthorizationPolicyFile) + if err != nil { + glog.Fatalf("Invalid Authorization Config: %v", err) + } + + admissionControlPluginNames := strings.Split(s.AdmissionControl, ",") + admissionController := admission.NewFromPlugins(client, admissionControlPluginNames, s.AdmissionControlConfigFile) + + if len(s.ExternalHost) == 0 { + // TODO: extend for other providers + if s.CloudProvider == "gce" { + instances, supported := cloud.Instances() + if !supported { + glog.Fatalf("gce cloud provider has no instances. this shouldn't happen. exiting.") + } + name, err := os.Hostname() + if err != nil { + glog.Fatalf("failed to get hostname: %v", err) + } + addrs, err := instances.NodeAddresses(name) + if err != nil { + glog.Warningf("unable to obtain external host address from cloud provider: %v", err) + } else { + for _, addr := range addrs { + if addr.Type == api.NodeExternalIP { + s.ExternalHost = addr.Address + } + } + } + } + } + + config := &master.Config{ + EtcdHelper: helper, + EventTTL: s.EventTTL, + KubeletClient: kubeletClient, + PortalNet: &n, + EnableCoreControllers: true, + EnableLogsSupport: s.EnableLogsSupport, + EnableUISupport: true, + EnableSwaggerSupport: true, + EnableProfiling: s.EnableProfiling, + EnableIndex: true, + APIPrefix: s.APIPrefix, + CorsAllowedOriginList: s.CorsAllowedOriginList, + ReadOnlyPort: s.ReadOnlyPort, + ReadWritePort: s.SecurePort, + PublicAddress: net.IP(s.BindAddress), + Authenticator: authenticator, + SupportsBasicAuth: len(s.BasicAuthFile) > 0, + Authorizer: authorizer, + AdmissionControl: admissionController, + DisableLegacyAPIs: disableLegacyAPIs, + DisableV1Beta3: disableV1beta3, + EnableV1: enableV1, + MasterServiceNamespace: s.MasterServiceNamespace, + ClusterName: s.ClusterName, + ExternalHost: s.ExternalHost, + } + m := master.New(config) + + // We serve on 3 ports. See docs/accessing_the_api.md + roLocation := "" + if s.ReadOnlyPort != 0 { + roLocation = net.JoinHostPort(s.BindAddress.String(), strconv.Itoa(s.ReadOnlyPort)) + } + secureLocation := "" + if s.SecurePort != 0 { + secureLocation = net.JoinHostPort(s.BindAddress.String(), strconv.Itoa(s.SecurePort)) + } + insecureLocation := net.JoinHostPort(s.InsecureBindAddress.String(), strconv.Itoa(s.InsecurePort)) + + // See the flag commentary to understand our assumptions when opening the read-only and read-write ports. + + var sem chan bool + if s.MaxRequestsInFlight > 0 { + sem = make(chan bool, s.MaxRequestsInFlight) + } + + longRunningRE := regexp.MustCompile(s.LongRunningRequestRE) + + if roLocation != "" { + // Default settings allow 1 read-only request per second, allow up to 20 in a burst before enforcing. + rl := util.NewTokenBucketRateLimiter(s.APIRate, s.APIBurst) + readOnlyServer := &http.Server{ + Addr: roLocation, + Handler: apiserver.MaxInFlightLimit(sem, longRunningRE, apiserver.RecoverPanics(apiserver.ReadOnly(apiserver.RateLimit(rl, m.InsecureHandler)))), + ReadTimeout: 5 * time.Minute, + WriteTimeout: 5 * time.Minute, + MaxHeaderBytes: 1 << 20, + } + glog.Infof("Serving read-only insecurely on %s", roLocation) + go func() { + defer util.HandleCrash() + for { + if err := readOnlyServer.ListenAndServe(); err != nil { + glog.Errorf("Unable to listen for read only traffic (%v); will try again.", err) + } + time.Sleep(15 * time.Second) + } + }() + } + + if secureLocation != "" { + secureServer := &http.Server{ + Addr: secureLocation, + Handler: apiserver.MaxInFlightLimit(sem, longRunningRE, apiserver.RecoverPanics(m.Handler)), + ReadTimeout: 5 * time.Minute, + WriteTimeout: 5 * time.Minute, + MaxHeaderBytes: 1 << 20, + TLSConfig: &tls.Config{ + // Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability) + MinVersion: tls.VersionTLS10, + }, + } + + if len(s.ClientCAFile) > 0 { + clientCAs, err := util.CertPoolFromFile(s.ClientCAFile) + if err != nil { + glog.Fatalf("unable to load client CA file: %v", err) + } + // Populate PeerCertificates in requests, but don't reject connections without certificates + // This allows certificates to be validated by authenticators, while still allowing other auth types + secureServer.TLSConfig.ClientAuth = tls.RequestClientCert + // Specify allowed CAs for client certificates + secureServer.TLSConfig.ClientCAs = clientCAs + } + + glog.Infof("Serving securely on %s", secureLocation) + go func() { + defer util.HandleCrash() + for { + if s.TLSCertFile == "" && s.TLSPrivateKeyFile == "" { + s.TLSCertFile = path.Join(s.CertDirectory, "apiserver.crt") + s.TLSPrivateKeyFile = path.Join(s.CertDirectory, "apiserver.key") + if err := util.GenerateSelfSignedCert(config.PublicAddress.String(), s.TLSCertFile, s.TLSPrivateKeyFile); err != nil { + glog.Errorf("Unable to generate self signed cert: %v", err) + } else { + glog.Infof("Using self-signed cert (%s, %s)", s.TLSCertFile, s.TLSPrivateKeyFile) + } + } + if err := secureServer.ListenAndServeTLS(s.TLSCertFile, s.TLSPrivateKeyFile); err != nil { + glog.Errorf("Unable to listen for secure (%v); will try again.", err) + } + time.Sleep(15 * time.Second) + } + }() + } + + http := &http.Server{ + Addr: insecureLocation, + Handler: apiserver.RecoverPanics(m.InsecureHandler), + ReadTimeout: 5 * time.Minute, + WriteTimeout: 5 * time.Minute, + MaxHeaderBytes: 1 << 20, + } + glog.Infof("Serving insecurely on %s", insecureLocation) + glog.Fatal(http.ListenAndServe()) + return nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-controller-manager/app/controllermanager.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-controller-manager/app/controllermanager.go new file mode 100644 index 000000000000..25b35b73f103 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-controller-manager/app/controllermanager.go @@ -0,0 +1,272 @@ +/* +Copyright 2014 The Kubernetes Authors 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 app implements a server that runs a set of active +// components. This includes replication controllers, service endpoints and +// nodes. +package app + +import ( + "net" + "net/http" + "net/http/pprof" + "strconv" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/nodecontroller" + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/servicecontroller" + replicationControllerPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/controller" + "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" + "github.com/GoogleCloudPlatform/kubernetes/pkg/master/ports" + "github.com/GoogleCloudPlatform/kubernetes/pkg/namespace" + "github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota" + "github.com/GoogleCloudPlatform/kubernetes/pkg/service" + "github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder" + + "github.com/golang/glog" + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/pflag" +) + +// CMServer is the main context object for the controller manager. +type CMServer struct { + Port int + Address util.IP + CloudProvider string + CloudConfigFile string + ConcurrentEndpointSyncs int + ConcurrentRCSyncs int + MinionRegexp string + NodeSyncPeriod time.Duration + ResourceQuotaSyncPeriod time.Duration + NamespaceSyncPeriod time.Duration + PVClaimBinderSyncPeriod time.Duration + RegisterRetryCount int + MachineList util.StringList + SyncNodeList bool + SyncNodeStatus bool + NodeMonitorGracePeriod time.Duration + NodeStartupGracePeriod time.Duration + NodeMonitorPeriod time.Duration + NodeStatusUpdateRetry int + PodEvictionTimeout time.Duration + DeletingPodsQps float32 + DeletingPodsBurst int + ServiceAccountKeyFile string + + // TODO: Discover these by pinging the host machines, and rip out these params. + NodeMilliCPU int64 + NodeMemory resource.Quantity + + ClusterName string + ClusterCIDR util.IPNet + AllocateNodeCIDRs bool + EnableProfiling bool + + Master string + Kubeconfig string +} + +// NewCMServer creates a new CMServer with a default config. +func NewCMServer() *CMServer { + s := CMServer{ + Port: ports.ControllerManagerPort, + Address: util.IP(net.ParseIP("127.0.0.1")), + ConcurrentEndpointSyncs: 5, + ConcurrentRCSyncs: 5, + NodeSyncPeriod: 10 * time.Second, + ResourceQuotaSyncPeriod: 10 * time.Second, + NamespaceSyncPeriod: 5 * time.Minute, + PVClaimBinderSyncPeriod: 10 * time.Second, + RegisterRetryCount: 10, + PodEvictionTimeout: 5 * time.Minute, + NodeMilliCPU: 1000, + NodeMemory: resource.MustParse("3Gi"), + SyncNodeList: true, + ClusterName: "kubernetes", + } + return &s +} + +// AddFlags adds flags for a specific CMServer to the specified FlagSet +func (s *CMServer) AddFlags(fs *pflag.FlagSet) { + fs.IntVar(&s.Port, "port", s.Port, "The port that the controller-manager's http service runs on") + fs.Var(&s.Address, "address", "The IP address to serve on (set to 0.0.0.0 for all interfaces)") + fs.StringVar(&s.CloudProvider, "cloud-provider", s.CloudProvider, "The provider for cloud services. Empty string for no provider.") + fs.StringVar(&s.CloudConfigFile, "cloud-config", s.CloudConfigFile, "The path to the cloud provider configuration file. Empty string for no configuration file.") + fs.IntVar(&s.ConcurrentEndpointSyncs, "concurrent-endpoint-syncs", s.ConcurrentEndpointSyncs, "The number of endpoint syncing operations that will be done concurrently. Larger number = faster endpoint updating, but more CPU (and network) load") + fs.IntVar(&s.ConcurrentRCSyncs, "concurrent_rc_syncs", s.ConcurrentRCSyncs, "The number of replication controllers that are allowed to sync concurrently. Larger number = more reponsive replica management, but more CPU (and network) load") + fs.StringVar(&s.MinionRegexp, "minion-regexp", s.MinionRegexp, "If non empty, and --cloud-provider is specified, a regular expression for matching minion VMs.") + fs.DurationVar(&s.NodeSyncPeriod, "node-sync-period", s.NodeSyncPeriod, ""+ + "The period for syncing nodes from cloudprovider. Longer periods will result in "+ + "fewer calls to cloud provider, but may delay addition of new nodes to cluster.") + fs.DurationVar(&s.ResourceQuotaSyncPeriod, "resource-quota-sync-period", s.ResourceQuotaSyncPeriod, "The period for syncing quota usage status in the system") + fs.DurationVar(&s.NamespaceSyncPeriod, "namespace-sync-period", s.NamespaceSyncPeriod, "The period for syncing namespace life-cycle updates") + fs.DurationVar(&s.PVClaimBinderSyncPeriod, "pvclaimbinder-sync-period", s.PVClaimBinderSyncPeriod, "The period for syncing persistent volumes and persistent volume claims") + fs.DurationVar(&s.PodEvictionTimeout, "pod-eviction-timeout", s.PodEvictionTimeout, "The grace peroid for deleting pods on failed nodes.") + fs.Float32Var(&s.DeletingPodsQps, "deleting-pods-qps", 0.1, "Number of nodes per second on which pods are deleted in case of node failure.") + fs.IntVar(&s.DeletingPodsBurst, "deleting-pods-burst", 10, "Number of nodes on which pods are bursty deleted in case of node failure. For more details look into RateLimiter.") + fs.IntVar(&s.RegisterRetryCount, "register-retry-count", s.RegisterRetryCount, ""+ + "The number of retries for initial node registration. Retry interval equals node-sync-period.") + fs.Var(&s.MachineList, "machines", "List of machines to schedule onto, comma separated.") + fs.BoolVar(&s.SyncNodeList, "sync-nodes", s.SyncNodeList, "If true, and --cloud-provider is specified, sync nodes from the cloud provider. Default true.") + fs.BoolVar(&s.SyncNodeStatus, "sync-node-status", s.SyncNodeStatus, + "DEPRECATED. Does not have any effect now and it will be removed in a later release.") + fs.DurationVar(&s.NodeMonitorGracePeriod, "node-monitor-grace-period", 40*time.Second, + "Amount of time which we allow running Node to be unresponsive before marking it unhealty. "+ + "Must be N times more than kubelet's nodeStatusUpdateFrequency, "+ + "where N means number of retries allowed for kubelet to post node status.") + fs.DurationVar(&s.NodeStartupGracePeriod, "node-startup-grace-period", 60*time.Second, + "Amount of time which we allow starting Node to be unresponsive before marking it unhealty.") + fs.DurationVar(&s.NodeMonitorPeriod, "node-monitor-period", 5*time.Second, + "The period for syncing NodeStatus in NodeController.") + fs.StringVar(&s.ServiceAccountKeyFile, "service-account-private-key-file", s.ServiceAccountKeyFile, "Filename containing a PEM-encoded private RSA key used to sign service account tokens.") + // TODO: Discover these by pinging the host machines, and rip out these flags. + // TODO: in the meantime, use resource.QuantityFlag() instead of these + fs.Int64Var(&s.NodeMilliCPU, "node-milli-cpu", s.NodeMilliCPU, "The amount of MilliCPU provisioned on each node") + fs.Var(resource.NewQuantityFlagValue(&s.NodeMemory), "node-memory", "The amount of memory (in bytes) provisioned on each node") + fs.StringVar(&s.ClusterName, "cluster-name", s.ClusterName, "The instance prefix for the cluster") + fs.BoolVar(&s.EnableProfiling, "profiling", true, "Enable profiling via web interface host:port/debug/pprof/") + fs.Var(&s.ClusterCIDR, "cluster-cidr", "CIDR Range for Pods in cluster.") + fs.BoolVar(&s.AllocateNodeCIDRs, "allocate-node-cidrs", false, "Should CIDRs for Pods be allocated and set on the cloud provider.") + fs.StringVar(&s.Master, "master", s.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig)") + fs.StringVar(&s.Kubeconfig, "kubeconfig", s.Kubeconfig, "Path to kubeconfig file with authorization and master location information.") +} + +func (s *CMServer) verifyMinionFlags() { + if !s.SyncNodeList && s.MinionRegexp != "" { + glog.Info("--minion-regexp is ignored by --sync-nodes=false") + } + if s.CloudProvider == "" || s.MinionRegexp == "" { + if len(s.MachineList) == 0 { + glog.Info("No machines specified!") + } + return + } + if len(s.MachineList) != 0 { + glog.Info("--machines is overwritten by --minion-regexp") + } +} + +// Run runs the CMServer. This should never exit. +func (s *CMServer) Run(_ []string) error { + s.verifyMinionFlags() + + if s.Kubeconfig == "" && s.Master == "" { + glog.Warningf("Neither --kubeconfig nor --master was specified. Using default API client. This might not work.") + } + + // This creates a client, first loading any specified kubeconfig + // file, and then overriding the Master flag, if non-empty. + kubeconfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: s.Kubeconfig}, + &clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: s.Master}}).ClientConfig() + if err != nil { + return err + } + + kubeconfig.QPS = 20.0 + kubeconfig.Burst = 30 + + kubeClient, err := client.New(kubeconfig) + if err != nil { + glog.Fatalf("Invalid API configuration: %v", err) + } + + go func() { + mux := http.NewServeMux() + healthz.InstallHandler(mux) + if s.EnableProfiling { + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + } + mux.Handle("/metrics", prometheus.Handler()) + + server := &http.Server{ + Addr: net.JoinHostPort(s.Address.String(), strconv.Itoa(s.Port)), + Handler: mux, + } + glog.Fatal(server.ListenAndServe()) + }() + + endpoints := service.NewEndpointController(kubeClient) + go endpoints.Run(s.ConcurrentEndpointSyncs, util.NeverStop) + + controllerManager := replicationControllerPkg.NewReplicationManager(kubeClient, replicationControllerPkg.BurstReplicas) + go controllerManager.Run(s.ConcurrentRCSyncs, util.NeverStop) + + cloud := cloudprovider.InitCloudProvider(s.CloudProvider, s.CloudConfigFile) + nodeResources := &api.NodeResources{ + Capacity: api.ResourceList{ + api.ResourceCPU: *resource.NewMilliQuantity(s.NodeMilliCPU, resource.DecimalSI), + api.ResourceMemory: s.NodeMemory, + }, + } + + if s.SyncNodeStatus { + glog.Warning("DEPRECATION NOTICE: sync-node-status flag is being deprecated. It has no effect now and it will be removed in a future version.") + } + + nodeController := nodecontroller.NewNodeController(cloud, s.MinionRegexp, s.MachineList, nodeResources, + kubeClient, s.RegisterRetryCount, s.PodEvictionTimeout, util.NewTokenBucketRateLimiter(s.DeletingPodsQps, s.DeletingPodsBurst), + s.NodeMonitorGracePeriod, s.NodeStartupGracePeriod, s.NodeMonitorPeriod, s.ClusterName, (*net.IPNet)(&s.ClusterCIDR), s.AllocateNodeCIDRs) + nodeController.Run(s.NodeSyncPeriod, s.SyncNodeList) + + serviceController := servicecontroller.New(cloud, kubeClient, s.ClusterName) + if err := serviceController.Run(s.NodeSyncPeriod); err != nil { + glog.Errorf("Failed to start service controller: %v", err) + } + + resourceQuotaManager := resourcequota.NewResourceQuotaManager(kubeClient) + resourceQuotaManager.Run(s.ResourceQuotaSyncPeriod) + + namespaceManager := namespace.NewNamespaceManager(kubeClient, s.NamespaceSyncPeriod) + namespaceManager.Run() + + pvclaimBinder := volumeclaimbinder.NewPersistentVolumeClaimBinder(kubeClient, s.PVClaimBinderSyncPeriod) + pvclaimBinder.Run() + + if len(s.ServiceAccountKeyFile) > 0 { + privateKey, err := serviceaccount.ReadPrivateKey(s.ServiceAccountKeyFile) + if err != nil { + glog.Errorf("Error reading key for service account token controller: %v", err) + } else { + serviceaccount.NewTokensController( + kubeClient, + serviceaccount.DefaultTokenControllerOptions( + serviceaccount.JWTTokenGenerator(privateKey), + ), + ).Run() + } + } + + serviceaccount.NewServiceAccountsController( + kubeClient, + serviceaccount.DefaultServiceAccountControllerOptions(), + ).Run() + + select {} + return nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-controller-manager/app/plugins.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-controller-manager/app/plugins.go new file mode 100644 index 000000000000..08cb0a4aec0f --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-controller-manager/app/plugins.go @@ -0,0 +1,29 @@ +/* +Copyright 2014 The Kubernetes Authors 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 app + +import ( + // This file exists to force the desired plugin implementations to be linked. + // This should probably be part of some configuration fed into the build for a + // given binary target. + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws" + _ "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/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-proxy/app/server.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-proxy/app/server.go new file mode 100644 index 000000000000..08e166d0adbe --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kube-proxy/app/server.go @@ -0,0 +1,150 @@ +/* +Copyright 2014 The Kubernetes Authors 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 app does all of the work necessary to configure and run a +// Kubernetes app process. +package app + +import ( + "net" + "net/http" + _ "net/http/pprof" + "strconv" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/proxy" + "github.com/GoogleCloudPlatform/kubernetes/pkg/proxy/config" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/exec" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/iptables" + + "github.com/golang/glog" + "github.com/spf13/pflag" +) + +// ProxyServer contains configures and runs a Kubernetes proxy server +type ProxyServer struct { + BindAddress util.IP + HealthzPort int + HealthzBindAddress util.IP + OOMScoreAdj int + ResourceContainer string + Master string + Kubeconfig string +} + +// NewProxyServer creates a new ProxyServer object with default parameters +func NewProxyServer() *ProxyServer { + return &ProxyServer{ + BindAddress: util.IP(net.ParseIP("0.0.0.0")), + HealthzPort: 10249, + HealthzBindAddress: util.IP(net.ParseIP("127.0.0.1")), + OOMScoreAdj: -899, + ResourceContainer: "/kube-proxy", + } +} + +// AddFlags adds flags for a specific ProxyServer to the specified FlagSet +func (s *ProxyServer) AddFlags(fs *pflag.FlagSet) { + fs.Var(&s.BindAddress, "bind-address", "The IP address for the proxy server to serve on (set to 0.0.0.0 for all interfaces)") + fs.StringVar(&s.Master, "master", s.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig)") + fs.IntVar(&s.HealthzPort, "healthz-port", s.HealthzPort, "The port to bind the health check server. Use 0 to disable.") + fs.Var(&s.HealthzBindAddress, "healthz-bind-address", "The IP address for the health check server to serve on, defaulting to 127.0.0.1 (set to 0.0.0.0 for all interfaces)") + fs.IntVar(&s.OOMScoreAdj, "oom-score-adj", s.OOMScoreAdj, "The oom_score_adj value for kube-proxy process. Values must be within the range [-1000, 1000]") + fs.StringVar(&s.ResourceContainer, "resource-container", s.ResourceContainer, "Absolute name of the resource-only container to create and run the Kube-proxy in (Default: /kube-proxy).") + fs.StringVar(&s.Kubeconfig, "kubeconfig", s.Kubeconfig, "Path to kubeconfig file with authorization information (the master location is set by the master flag).") +} + +// Run runs the specified ProxyServer. This should never exit. +func (s *ProxyServer) Run(_ []string) error { + // TODO(vmarmol): Use container config for this. + if err := util.ApplyOomScoreAdj(0, s.OOMScoreAdj); err != nil { + glog.Info(err) + } + + // Run in its own container. + if err := util.RunInResourceContainer(s.ResourceContainer); err != nil { + glog.Warningf("Failed to start in resource-only container %q: %v", s.ResourceContainer, err) + } else { + glog.Infof("Running in resource-only container %q", s.ResourceContainer) + } + + serviceConfig := config.NewServiceConfig() + endpointsConfig := config.NewEndpointsConfig() + + protocol := iptables.ProtocolIpv4 + if net.IP(s.BindAddress).To4() == nil { + protocol = iptables.ProtocolIpv6 + } + loadBalancer := proxy.NewLoadBalancerRR() + proxier := proxy.NewProxier(loadBalancer, net.IP(s.BindAddress), iptables.New(exec.New(), protocol)) + if proxier == nil { + glog.Fatalf("failed to create proxier, aborting") + } + + // Wire proxier to handle changes to services + serviceConfig.RegisterHandler(proxier) + // And wire loadBalancer to handle changes to endpoints to services + endpointsConfig.RegisterHandler(loadBalancer) + + // Note: RegisterHandler() calls need to happen before creation of Sources because sources + // only notify on changes, and the initial update (on process start) may be lost if no handlers + // are registered yet. + + // define api config source + if s.Kubeconfig == "" && s.Master == "" { + glog.Warningf("Neither --kubeconfig nor --master was specified. Using default API client. This might not work.") + } + + // This creates a client, first loading any specified kubeconfig + // file, and then overriding the Master flag, if non-empty. + kubeconfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: s.Kubeconfig}, + &clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: s.Master}}).ClientConfig() + if err != nil { + return err + } + + client, err := client.New(kubeconfig) + if err != nil { + glog.Fatalf("Invalid API configuration: %v", err) + } + + config.NewSourceAPI( + client.Services(api.NamespaceAll), + client.Endpoints(api.NamespaceAll), + 30*time.Second, + serviceConfig.Channel("api"), + endpointsConfig.Channel("api"), + ) + + if s.HealthzPort > 0 { + go util.Forever(func() { + err := http.ListenAndServe(s.HealthzBindAddress.String()+":"+strconv.Itoa(s.HealthzPort), nil) + if err != nil { + glog.Errorf("Starting health server failed: %v", err) + } + }, 5*time.Second) + } + + // Just loop forever for now... + proxier.SyncLoop() + return nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kubelet/app/plugins.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kubelet/app/plugins.go new file mode 100644 index 000000000000..66157108f4cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kubelet/app/plugins.go @@ -0,0 +1,76 @@ +/* +Copyright 2014 The Kubernetes Authors 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 app + +// This file exists to force the desired plugin implementations to be linked. +import ( + // Credential providers + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider/gcp" + // Network plugins + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/network" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/network/exec" + // Volume plugins + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/aws_ebs" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/empty_dir" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/gce_pd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/git_repo" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/glusterfs" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/host_path" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/iscsi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/nfs" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/persistent_claim" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/secret" + //Cloud providers + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws" + _ "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" +) + +// ProbeVolumePlugins collects all volume plugins into an easy to use list. +func ProbeVolumePlugins() []volume.VolumePlugin { + allPlugins := []volume.VolumePlugin{} + + // The list of plugins to probe is decided by the kubelet binary, not + // by dynamic linking or other "magic". Plugins will be analyzed and + // initialized later. + allPlugins = append(allPlugins, aws_ebs.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, empty_dir.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, gce_pd.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, git_repo.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, host_path.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, nfs.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, secret.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, iscsi.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, glusterfs.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, persistent_claim.ProbeVolumePlugins()...) + + return allPlugins +} + +// ProbeNetworkPlugins collects all compiled-in plugins +func ProbeNetworkPlugins() []network.NetworkPlugin { + allPlugins := []network.NetworkPlugin{} + + // for each existing plugin, add to the list + allPlugins = append(allPlugins, exec.ProbeNetworkPlugins()...) + + return allPlugins +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kubelet/app/server.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kubelet/app/server.go new file mode 100644 index 000000000000..1f67e225abda --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/cmd/kubelet/app/server.go @@ -0,0 +1,691 @@ +/* +Copyright 2014 The Kubernetes Authors 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 app makes it easy to create a kubelet server for various contexts. +package app + +import ( + "crypto/tls" + "fmt" + "math/rand" + "net" + "net/http" + _ "net/http/pprof" + "path" + "strconv" + "strings" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/chaosclient" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" + "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" + "github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider" + "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/cadvisor" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/config" + kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/network" + "github.com/GoogleCloudPlatform/kubernetes/pkg/master/ports" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" + "github.com/golang/glog" + "github.com/spf13/pflag" +) + +const defaultRootDir = "/var/lib/kubelet" + +// KubeletServer encapsulates all of the parameters necessary for starting up +// a kubelet. These can either be set via command line or directly. +type KubeletServer struct { + Config string + SyncFrequency time.Duration + FileCheckFrequency time.Duration + HTTPCheckFrequency time.Duration + ManifestURL string + EnableServer bool + Address util.IP + Port uint + ReadOnlyPort uint + HostnameOverride string + PodInfraContainerImage string + DockerEndpoint string + RootDirectory string + AllowPrivileged bool + HostNetworkSources string + RegistryPullQPS float64 + RegistryBurst int + RunOnce bool + EnableDebuggingHandlers bool + MinimumGCAge time.Duration + MaxPerPodContainerCount int + MaxContainerCount int + AuthPath util.StringFlag // Deprecated -- use KubeConfig instead + KubeConfig util.StringFlag + CadvisorPort uint + HealthzPort int + HealthzBindAddress util.IP + OOMScoreAdj int + APIServerList util.StringList + ClusterDomain string + MasterServiceNamespace string + ClusterDNS util.IP + StreamingConnectionIdleTimeout time.Duration + ImageGCHighThresholdPercent int + ImageGCLowThresholdPercent int + LowDiskSpaceThresholdMB int + NetworkPluginName string + CloudProvider string + CloudConfigFile string + TLSCertFile string + TLSPrivateKeyFile string + CertDirectory string + NodeStatusUpdateFrequency time.Duration + ResourceContainer string + CgroupRoot string + ContainerRuntime string + DockerDaemonContainer string + + // Flags intended for testing + + // Crash immediately, rather than eating panics. + ReallyCrashForTesting bool + // Insert a probability of random errors during calls to the master. + ChaosChance float64 + // Is the kubelet containerized? + Containerized bool +} + +// bootstrapping interface for kubelet, targets the initialization protocol +type KubeletBootstrap interface { + BirthCry() + StartGarbageCollection() + ListenAndServe(net.IP, uint, *kubelet.TLSOptions, bool) + ListenAndServeReadOnly(net.IP, uint) + Run(<-chan kubelet.PodUpdate) + RunOnce(<-chan kubelet.PodUpdate) ([]kubelet.RunPodResult, error) +} + +// create and initialize a Kubelet instance +type KubeletBuilder func(kc *KubeletConfig) (KubeletBootstrap, *config.PodConfig, error) + +// NewKubeletServer will create a new KubeletServer with default values. +func NewKubeletServer() *KubeletServer { + return &KubeletServer{ + SyncFrequency: 10 * time.Second, + FileCheckFrequency: 20 * time.Second, + HTTPCheckFrequency: 20 * time.Second, + EnableServer: true, + Address: util.IP(net.ParseIP("0.0.0.0")), + Port: ports.KubeletPort, + ReadOnlyPort: ports.KubeletReadOnlyPort, + PodInfraContainerImage: dockertools.PodInfraContainerImage, + RootDirectory: defaultRootDir, + RegistryBurst: 10, + EnableDebuggingHandlers: true, + MinimumGCAge: 1 * time.Minute, + MaxPerPodContainerCount: 5, + MaxContainerCount: 100, + AuthPath: util.NewStringFlag("/var/lib/kubelet/kubernetes_auth"), // deprecated + KubeConfig: util.NewStringFlag("/var/lib/kubelet/kubeconfig"), + CadvisorPort: 4194, + HealthzPort: 10248, + HealthzBindAddress: util.IP(net.ParseIP("127.0.0.1")), + OOMScoreAdj: -900, + MasterServiceNamespace: api.NamespaceDefault, + ImageGCHighThresholdPercent: 90, + ImageGCLowThresholdPercent: 80, + LowDiskSpaceThresholdMB: 256, + NetworkPluginName: "", + HostNetworkSources: kubelet.FileSource, + CertDirectory: "/var/run/kubernetes", + NodeStatusUpdateFrequency: 10 * time.Second, + ResourceContainer: "/kubelet", + CgroupRoot: "/", + ContainerRuntime: "docker", + DockerDaemonContainer: "/docker-daemon", + } +} + +// AddFlags adds flags for a specific KubeletServer to the specified FlagSet +func (s *KubeletServer) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&s.Config, "config", s.Config, "Path to the config file or directory of files") + fs.DurationVar(&s.SyncFrequency, "sync-frequency", s.SyncFrequency, "Max period between synchronizing running containers and config") + fs.DurationVar(&s.FileCheckFrequency, "file-check-frequency", s.FileCheckFrequency, "Duration between checking config files for new data") + fs.DurationVar(&s.HTTPCheckFrequency, "http-check-frequency", s.HTTPCheckFrequency, "Duration between checking http for new data") + fs.StringVar(&s.ManifestURL, "manifest-url", s.ManifestURL, "URL for accessing the container manifest") + fs.BoolVar(&s.EnableServer, "enable-server", s.EnableServer, "Enable the info server") + fs.Var(&s.Address, "address", "The IP address for the info server to serve on (set to 0.0.0.0 for all interfaces)") + fs.UintVar(&s.Port, "port", s.Port, "The port for the info server to serve on") + fs.UintVar(&s.ReadOnlyPort, "read-only-port", s.ReadOnlyPort, "The read-only port for the info server to serve on (set to 0 to disable)") + fs.StringVar(&s.TLSCertFile, "tls-cert-file", s.TLSCertFile, ""+ + "File containing x509 Certificate for HTTPS. (CA cert, if any, concatenated after server cert). "+ + "If --tls_cert_file and --tls_private_key_file are not provided, a self-signed certificate and key "+ + "are generated for the public address and saved to the directory passed to --cert_dir.") + fs.StringVar(&s.TLSPrivateKeyFile, "tls-private-key-file", s.TLSPrivateKeyFile, "File containing x509 private key matching --tls_cert_file.") + fs.StringVar(&s.CertDirectory, "cert-dir", s.CertDirectory, "The directory where the TLS certs are located (by default /var/run/kubernetes). "+ + "If --tls_cert_file and --tls_private_key_file are provided, this flag will be ignored.") + fs.StringVar(&s.HostnameOverride, "hostname-override", s.HostnameOverride, "If non-empty, will use this string as identification instead of the actual hostname.") + fs.StringVar(&s.PodInfraContainerImage, "pod-infra-container-image", s.PodInfraContainerImage, "The image whose network/ipc namespaces containers in each pod will use.") + fs.StringVar(&s.DockerEndpoint, "docker-endpoint", s.DockerEndpoint, "If non-empty, use this for the docker endpoint to communicate with") + fs.StringVar(&s.RootDirectory, "root-dir", s.RootDirectory, "Directory path for managing kubelet files (volume mounts,etc).") + fs.BoolVar(&s.AllowPrivileged, "allow-privileged", s.AllowPrivileged, "If true, allow containers to request privileged mode. [default=false]") + fs.StringVar(&s.HostNetworkSources, "host-network-sources", s.HostNetworkSources, "Comma-separated list of sources from which the Kubelet allows pods to use of host network. For all sources use \"*\" [default=\"file\"]") + fs.Float64Var(&s.RegistryPullQPS, "registry-qps", s.RegistryPullQPS, "If > 0, limit registry pull QPS to this value. If 0, unlimited. [default=0.0]") + fs.IntVar(&s.RegistryBurst, "registry-burst", s.RegistryBurst, "Maximum size of a bursty pulls, temporarily allows pulls to burst to this number, while still not exceeding registry_qps. Only used if --registry_qps > 0") + fs.BoolVar(&s.RunOnce, "runonce", s.RunOnce, "If true, exit after spawning pods from local manifests or remote urls. Exclusive with --api_servers, and --enable-server") + fs.BoolVar(&s.EnableDebuggingHandlers, "enable-debugging-handlers", s.EnableDebuggingHandlers, "Enables server endpoints for log collection and local running of containers and commands") + fs.DurationVar(&s.MinimumGCAge, "minimum-container-ttl-duration", s.MinimumGCAge, "Minimum age for a finished container before it is garbage collected. Examples: '300ms', '10s' or '2h45m'") + fs.IntVar(&s.MaxPerPodContainerCount, "maximum-dead-containers-per-container", s.MaxPerPodContainerCount, "Maximum number of old instances of a container to retain per container. Each container takes up some disk space. Default: 5.") + fs.IntVar(&s.MaxContainerCount, "maximum-dead-containers", s.MaxContainerCount, "Maximum number of old instances of a containers to retain globally. Each container takes up some disk space. Default: 100.") + fs.Var(&s.AuthPath, "auth-path", "Path to .kubernetes_auth file, specifying how to authenticate to API server.") + fs.MarkDeprecated("auth-path", "will be removed in a future version") + fs.Var(&s.KubeConfig, "kubeconfig", "Path to a kubeconfig file, specifying how to authenticate to API server (the master location is set by the api-servers flag).") + fs.UintVar(&s.CadvisorPort, "cadvisor-port", s.CadvisorPort, "The port of the localhost cAdvisor endpoint") + fs.IntVar(&s.HealthzPort, "healthz-port", s.HealthzPort, "The port of the localhost healthz endpoint") + fs.Var(&s.HealthzBindAddress, "healthz-bind-address", "The IP address for the healthz server to serve on, defaulting to 127.0.0.1 (set to 0.0.0.0 for all interfaces)") + fs.IntVar(&s.OOMScoreAdj, "oom-score-adj", s.OOMScoreAdj, "The oom_score_adj value for kubelet process. Values must be within the range [-1000, 1000]") + fs.Var(&s.APIServerList, "api-servers", "List of Kubernetes API servers for publishing events, and reading pods and services. (ip:port), comma separated.") + fs.StringVar(&s.ClusterDomain, "cluster-domain", s.ClusterDomain, "Domain for this cluster. If set, kubelet will configure all containers to search this domain in addition to the host's search domains") + fs.StringVar(&s.MasterServiceNamespace, "master-service-namespace", s.MasterServiceNamespace, "The namespace from which the kubernetes master services should be injected into pods") + fs.Var(&s.ClusterDNS, "cluster-dns", "IP address for a cluster DNS server. If set, kubelet will configure all containers to use this for DNS resolution in addition to the host's DNS servers") + fs.DurationVar(&s.StreamingConnectionIdleTimeout, "streaming-connection-idle-timeout", 0, "Maximum time a streaming connection can be idle before the connection is automatically closed. Example: '5m'") + fs.DurationVar(&s.NodeStatusUpdateFrequency, "node-status-update-frequency", s.NodeStatusUpdateFrequency, "Specifies how often kubelet posts node status to master. Note: be cautious when changing the constant, it must work with nodeMonitorGracePeriod in nodecontroller. Default: 10s") + fs.IntVar(&s.ImageGCHighThresholdPercent, "image-gc-high-threshold", s.ImageGCHighThresholdPercent, "The percent of disk usage after which image garbage collection is always run. Default: 90%%") + fs.IntVar(&s.ImageGCLowThresholdPercent, "image-gc-low-threshold", s.ImageGCLowThresholdPercent, "The percent of disk usage before which image garbage collection is never run. Lowest disk usage to garbage collect to. Default: 80%%") + fs.IntVar(&s.LowDiskSpaceThresholdMB, "low-diskspace-threshold-mb", s.LowDiskSpaceThresholdMB, "The absolute free disk space, in MB, to maintain. When disk space falls below this threshold, new pods would be rejected. Default: 256") + fs.StringVar(&s.NetworkPluginName, "network-plugin", s.NetworkPluginName, " The name of the network plugin to be invoked for various events in kubelet/pod lifecycle") + fs.StringVar(&s.CloudProvider, "cloud-provider", s.CloudProvider, "The provider for cloud services. Empty string for no provider.") + fs.StringVar(&s.CloudConfigFile, "cloud-config", s.CloudConfigFile, "The path to the cloud provider configuration file. Empty string for no configuration file.") + fs.StringVar(&s.ResourceContainer, "resource-container", s.ResourceContainer, "Absolute name of the resource-only container to create and run the Kubelet in (Default: /kubelet).") + fs.StringVar(&s.CgroupRoot, "cgroup_root", s.CgroupRoot, "Optional root cgroup to use for pods. This is handled by the container runtime on a best effort basis. Default: '/', which means top-level.") + fs.StringVar(&s.ContainerRuntime, "container_runtime", s.ContainerRuntime, "The container runtime to use. Possible values: 'docker', 'rkt'. Default: 'docker'.") + fs.StringVar(&s.DockerDaemonContainer, "docker-daemon-container", s.DockerDaemonContainer, "Optional resource-only container in which to place the Docker Daemon. Empty for no container (Default: /docker-daemon).") + + // Flags intended for testing, not recommended used in production environments. + fs.BoolVar(&s.ReallyCrashForTesting, "really-crash-for-testing", s.ReallyCrashForTesting, "If true, when panics occur crash. Intended for testing.") + fs.Float64Var(&s.ChaosChance, "chaos-chance", s.ChaosChance, "If > 0.0, introduce random client errors and latency. Intended for testing. [default=0.0]") + fs.BoolVar(&s.Containerized, "containerized", s.Containerized, "Experimental support for running kubelet in a container. Intended for testing. [default=false]") +} + +// Run runs the specified KubeletServer. This should never exit. +func (s *KubeletServer) Run(_ []string) error { + util.ReallyCrash = s.ReallyCrashForTesting + rand.Seed(time.Now().UTC().UnixNano()) + + // TODO(vmarmol): Do this through container config. + if err := util.ApplyOomScoreAdj(0, s.OOMScoreAdj); err != nil { + glog.Info(err) + } + + client, err := s.createAPIServerClient() + if err != nil && len(s.APIServerList) > 0 { + glog.Warningf("No API client: %v", err) + } + + glog.Infof("Using root directory: %v", s.RootDirectory) + + credentialprovider.SetPreferredDockercfgPath(s.RootDirectory) + + cadvisorInterface, err := cadvisor.New(s.CadvisorPort) + if err != nil { + return err + } + + imageGCPolicy := kubelet.ImageGCPolicy{ + HighThresholdPercent: s.ImageGCHighThresholdPercent, + LowThresholdPercent: s.ImageGCLowThresholdPercent, + } + + diskSpacePolicy := kubelet.DiskSpacePolicy{ + DockerFreeDiskMB: s.LowDiskSpaceThresholdMB, + RootFreeDiskMB: s.LowDiskSpaceThresholdMB, + } + cloud := cloudprovider.InitCloudProvider(s.CloudProvider, s.CloudConfigFile) + glog.Infof("Successfully initialized cloud provider: %q from the config file: %q\n", s.CloudProvider, s.CloudConfigFile) + + hostNetworkSources, err := kubelet.GetValidatedSources(strings.Split(s.HostNetworkSources, ",")) + if err != nil { + return err + } + + if s.TLSCertFile == "" && s.TLSPrivateKeyFile == "" { + s.TLSCertFile = path.Join(s.CertDirectory, "kubelet.crt") + s.TLSPrivateKeyFile = path.Join(s.CertDirectory, "kubelet.key") + if err := util.GenerateSelfSignedCert(util.GetHostname(s.HostnameOverride), s.TLSCertFile, s.TLSPrivateKeyFile); err != nil { + glog.Fatalf("Unable to generate self signed cert: %v", err) + } + glog.Infof("Using self-signed cert (%s, %s)", s.TLSCertFile, s.TLSPrivateKeyFile) + } + tlsOptions := &kubelet.TLSOptions{ + Config: &tls.Config{ + // Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability). + MinVersion: tls.VersionTLS10, + // Populate PeerCertificates in requests, but don't yet reject connections without certificates. + ClientAuth: tls.RequestClientCert, + }, + CertFile: s.TLSCertFile, + KeyFile: s.TLSPrivateKeyFile, + } + + mounter := mount.New() + if s.Containerized { + glog.Info("Running kubelet in containerized mode (experimental)") + mounter = &mount.NsenterMounter{} + } + + kcfg := KubeletConfig{ + Address: s.Address, + AllowPrivileged: s.AllowPrivileged, + HostNetworkSources: hostNetworkSources, + HostnameOverride: s.HostnameOverride, + RootDirectory: s.RootDirectory, + ConfigFile: s.Config, + ManifestURL: s.ManifestURL, + FileCheckFrequency: s.FileCheckFrequency, + HTTPCheckFrequency: s.HTTPCheckFrequency, + PodInfraContainerImage: s.PodInfraContainerImage, + SyncFrequency: s.SyncFrequency, + RegistryPullQPS: s.RegistryPullQPS, + RegistryBurst: s.RegistryBurst, + MinimumGCAge: s.MinimumGCAge, + MaxPerPodContainerCount: s.MaxPerPodContainerCount, + MaxContainerCount: s.MaxContainerCount, + ClusterDomain: s.ClusterDomain, + ClusterDNS: s.ClusterDNS, + Runonce: s.RunOnce, + Port: s.Port, + ReadOnlyPort: s.ReadOnlyPort, + CadvisorInterface: cadvisorInterface, + EnableServer: s.EnableServer, + EnableDebuggingHandlers: s.EnableDebuggingHandlers, + DockerClient: dockertools.ConnectToDockerOrDie(s.DockerEndpoint), + KubeClient: client, + MasterServiceNamespace: s.MasterServiceNamespace, + VolumePlugins: ProbeVolumePlugins(), + NetworkPlugins: ProbeNetworkPlugins(), + NetworkPluginName: s.NetworkPluginName, + StreamingConnectionIdleTimeout: s.StreamingConnectionIdleTimeout, + TLSOptions: tlsOptions, + ImageGCPolicy: imageGCPolicy, + DiskSpacePolicy: diskSpacePolicy, + Cloud: cloud, + NodeStatusUpdateFrequency: s.NodeStatusUpdateFrequency, + ResourceContainer: s.ResourceContainer, + CgroupRoot: s.CgroupRoot, + ContainerRuntime: s.ContainerRuntime, + Mounter: mounter, + DockerDaemonContainer: s.DockerDaemonContainer, + } + + RunKubelet(&kcfg, nil) + + if s.HealthzPort > 0 { + healthz.DefaultHealthz() + go util.Forever(func() { + err := http.ListenAndServe(net.JoinHostPort(s.HealthzBindAddress.String(), strconv.Itoa(s.HealthzPort)), nil) + if err != nil { + glog.Errorf("Starting health server failed: %v", err) + } + }, 5*time.Second) + } + + // runs forever + select {} + +} + +func (s *KubeletServer) authPathClientConfig(useDefaults bool) (*client.Config, error) { + authInfo, err := clientauth.LoadFromFile(s.AuthPath.Value()) + if err != nil && !useDefaults { + return nil, err + } + // If loading the default auth path, for backwards compatibility keep going + // with the default auth. + if err != nil { + glog.Warningf("Could not load kubernetes auth path %s: %v. Continuing with defaults.", s.AuthPath, err) + } + if authInfo == nil { + // authInfo didn't load correctly - continue with defaults. + authInfo = &clientauth.Info{} + } + authConfig, err := authInfo.MergeWithConfig(client.Config{}) + if err != nil { + return nil, err + } + authConfig.Host = s.APIServerList[0] + return &authConfig, nil +} + +func (s *KubeletServer) kubeconfigClientConfig() (*client.Config, error) { + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: s.KubeConfig.Value()}, + &clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: s.APIServerList[0]}}).ClientConfig() +} + +// createClientConfig creates a client configuration from the command line +// arguments. If either --auth-path or --kubeconfig is explicitly set, it +// will be used (setting both is an error). If neither are set first attempt +// to load the default kubeconfig file, then the default auth path file, and +// fall back to the default auth (none) without an error. +// TODO(roberthbailey): Remove support for --auth-path +func (s *KubeletServer) createClientConfig() (*client.Config, error) { + if s.KubeConfig.Provided() && s.AuthPath.Provided() { + return nil, fmt.Errorf("cannot specify both --kubeconfig and --auth-path") + } + if s.KubeConfig.Provided() { + return s.kubeconfigClientConfig() + } else if s.AuthPath.Provided() { + return s.authPathClientConfig(false) + } + // Try the kubeconfig default first, falling back to the auth path default. + clientConfig, err := s.kubeconfigClientConfig() + if err != nil { + glog.Warningf("Could not load kubeconfig file %s: %v. Trying auth path instead.", s.KubeConfig, err) + return s.authPathClientConfig(true) + } + return clientConfig, nil +} + +func (s *KubeletServer) createAPIServerClient() (*client.Client, error) { + if len(s.APIServerList) < 1 { + return nil, fmt.Errorf("no api servers specified") + } + // TODO: adapt Kube client to support LB over several servers + if len(s.APIServerList) > 1 { + glog.Infof("Multiple api servers specified. Picking first one") + } + + clientConfig, err := s.createClientConfig() + if err != nil { + return nil, err + } + s.addChaosToClientConfig(clientConfig) + client, err := client.New(clientConfig) + if err != nil { + return nil, err + } + return client, nil +} + +// addChaosToClientConfig injects random errors into client connections if configured. +func (s *KubeletServer) addChaosToClientConfig(config *client.Config) { + if s.ChaosChance != 0.0 { + config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + seed := chaosclient.NewSeed(1) + // TODO: introduce a standard chaos package with more tunables - this is just a proof of concept + // TODO: introduce random latency and stalls + return chaosclient.NewChaosRoundTripper(rt, chaosclient.LogChaos, seed.P(s.ChaosChance, chaosclient.ErrSimulatedConnectionResetByPeer)) + } + } +} + +// SimpleRunKubelet is a simple way to start a Kubelet talking to dockerEndpoint, using an API Client. +// Under the hood it calls RunKubelet (below) +func SimpleKubelet(client *client.Client, + dockerClient dockertools.DockerInterface, + hostname, rootDir, manifestURL, address string, + port uint, + masterServiceNamespace string, + volumePlugins []volume.VolumePlugin, + tlsOptions *kubelet.TLSOptions, + cadvisorInterface cadvisor.Interface, + configFilePath string, + cloud cloudprovider.Interface, + osInterface kubecontainer.OSInterface) *KubeletConfig { + + imageGCPolicy := kubelet.ImageGCPolicy{ + HighThresholdPercent: 90, + LowThresholdPercent: 80, + } + diskSpacePolicy := kubelet.DiskSpacePolicy{ + DockerFreeDiskMB: 256, + RootFreeDiskMB: 256, + } + kcfg := KubeletConfig{ + KubeClient: client, + DockerClient: dockerClient, + HostnameOverride: hostname, + RootDirectory: rootDir, + ManifestURL: manifestURL, + PodInfraContainerImage: dockertools.PodInfraContainerImage, + Port: port, + Address: util.IP(net.ParseIP(address)), + EnableServer: true, + EnableDebuggingHandlers: true, + HTTPCheckFrequency: 1 * time.Second, + FileCheckFrequency: 1 * time.Second, + SyncFrequency: 3 * time.Second, + MinimumGCAge: 10 * time.Second, + MaxPerPodContainerCount: 5, + MaxContainerCount: 100, + MasterServiceNamespace: masterServiceNamespace, + VolumePlugins: volumePlugins, + TLSOptions: tlsOptions, + CadvisorInterface: cadvisorInterface, + ConfigFile: configFilePath, + ImageGCPolicy: imageGCPolicy, + DiskSpacePolicy: diskSpacePolicy, + Cloud: cloud, + NodeStatusUpdateFrequency: 10 * time.Second, + ResourceContainer: "/kubelet", + OSInterface: osInterface, + CgroupRoot: "/", + ContainerRuntime: "docker", + Mounter: mount.New(), + DockerDaemonContainer: "/docker-daemon", + } + return &kcfg +} + +// RunKubelet is responsible for setting up and running a kubelet. It is used in three different applications: +// 1 Integration tests +// 2 Kubelet binary +// 3 Standalone 'kubernetes' binary +// Eventually, #2 will be replaced with instances of #3 +func RunKubelet(kcfg *KubeletConfig, builder KubeletBuilder) { + kcfg.Hostname = util.GetHostname(kcfg.HostnameOverride) + eventBroadcaster := record.NewBroadcaster() + kcfg.Recorder = eventBroadcaster.NewRecorder(api.EventSource{Component: "kubelet", Host: kcfg.Hostname}) + eventBroadcaster.StartLogging(glog.Infof) + if kcfg.KubeClient != nil { + glog.Infof("Sending events to api server.") + eventBroadcaster.StartRecordingToSink(kcfg.KubeClient.Events("")) + } else { + glog.Infof("No api server defined - no events will be sent to API server.") + } + capabilities.Setup(kcfg.AllowPrivileged, kcfg.HostNetworkSources) + + credentialprovider.SetPreferredDockercfgPath(kcfg.RootDirectory) + + if builder == nil { + builder = createAndInitKubelet + } + if kcfg.OSInterface == nil { + kcfg.OSInterface = kubecontainer.RealOS{} + } + k, podCfg, err := builder(kcfg) + if err != nil { + glog.Errorf("Failed to create kubelet: %s", err) + return + } + // process pods and exit. + if kcfg.Runonce { + if _, err := k.RunOnce(podCfg.Updates()); err != nil { + glog.Errorf("--runonce failed: %v", err) + } + } else { + startKubelet(k, podCfg, kcfg) + } + glog.Infof("Started kubelet") +} + +func startKubelet(k KubeletBootstrap, podCfg *config.PodConfig, kc *KubeletConfig) { + // start the kubelet + go util.Forever(func() { k.Run(podCfg.Updates()) }, 0) + + // start the kubelet server + if kc.EnableServer { + go util.Forever(func() { + k.ListenAndServe(net.IP(kc.Address), kc.Port, kc.TLSOptions, kc.EnableDebuggingHandlers) + }, 0) + } + if kc.ReadOnlyPort > 0 { + go util.Forever(func() { + k.ListenAndServeReadOnly(net.IP(kc.Address), kc.ReadOnlyPort) + }, 0) + } +} + +func makePodSourceConfig(kc *KubeletConfig) *config.PodConfig { + // source of all configuration + cfg := config.NewPodConfig(config.PodConfigNotificationSnapshotAndUpdates, kc.Recorder) + + // define file config source + if kc.ConfigFile != "" { + glog.Infof("Adding manifest file: %v", kc.ConfigFile) + config.NewSourceFile(kc.ConfigFile, kc.Hostname, kc.FileCheckFrequency, cfg.Channel(kubelet.FileSource)) + } + + // define url config source + if kc.ManifestURL != "" { + glog.Infof("Adding manifest url: %v", kc.ManifestURL) + config.NewSourceURL(kc.ManifestURL, kc.Hostname, kc.HTTPCheckFrequency, cfg.Channel(kubelet.HTTPSource)) + } + if kc.KubeClient != nil { + glog.Infof("Watching apiserver") + config.NewSourceApiserver(kc.KubeClient, kc.Hostname, cfg.Channel(kubelet.ApiserverSource)) + } + return cfg +} + +// KubeletConfig is all of the parameters necessary for running a kubelet. +// TODO: This should probably be merged with KubeletServer. The extra object is a consequence of refactoring. +type KubeletConfig struct { + KubeClient *client.Client + DockerClient dockertools.DockerInterface + CadvisorInterface cadvisor.Interface + Address util.IP + AllowPrivileged bool + HostNetworkSources []string + HostnameOverride string + RootDirectory string + ConfigFile string + ManifestURL string + FileCheckFrequency time.Duration + HTTPCheckFrequency time.Duration + Hostname string + PodInfraContainerImage string + SyncFrequency time.Duration + RegistryPullQPS float64 + RegistryBurst int + MinimumGCAge time.Duration + MaxPerPodContainerCount int + MaxContainerCount int + ClusterDomain string + ClusterDNS util.IP + EnableServer bool + EnableDebuggingHandlers bool + Port uint + ReadOnlyPort uint + Runonce bool + MasterServiceNamespace string + VolumePlugins []volume.VolumePlugin + NetworkPlugins []network.NetworkPlugin + NetworkPluginName string + StreamingConnectionIdleTimeout time.Duration + Recorder record.EventRecorder + TLSOptions *kubelet.TLSOptions + ImageGCPolicy kubelet.ImageGCPolicy + DiskSpacePolicy kubelet.DiskSpacePolicy + Cloud cloudprovider.Interface + NodeStatusUpdateFrequency time.Duration + ResourceContainer string + OSInterface kubecontainer.OSInterface + CgroupRoot string + ContainerRuntime string + Mounter mount.Interface + DockerDaemonContainer string +} + +func createAndInitKubelet(kc *KubeletConfig) (k KubeletBootstrap, pc *config.PodConfig, err error) { + // TODO: block until all sources have delivered at least one update to the channel, or break the sync loop + // up into "per source" synchronizations + // TODO: KubeletConfig.KubeClient should be a client interface, but client interface misses certain methods + // used by kubelet. Since NewMainKubelet expects a client interface, we need to make sure we are not passing + // a nil pointer to it when what we really want is a nil interface. + var kubeClient client.Interface + if kc.KubeClient != nil { + kubeClient = kc.KubeClient + } + + gcPolicy := kubelet.ContainerGCPolicy{ + MinAge: kc.MinimumGCAge, + MaxPerPodContainer: kc.MaxPerPodContainerCount, + MaxContainers: kc.MaxContainerCount, + } + + pc = makePodSourceConfig(kc) + k, err = kubelet.NewMainKubelet( + kc.Hostname, + kc.DockerClient, + kubeClient, + kc.RootDirectory, + kc.PodInfraContainerImage, + kc.SyncFrequency, + float32(kc.RegistryPullQPS), + kc.RegistryBurst, + gcPolicy, + pc.SeenAllSources, + kc.ClusterDomain, + net.IP(kc.ClusterDNS), + kc.MasterServiceNamespace, + kc.VolumePlugins, + kc.NetworkPlugins, + kc.NetworkPluginName, + kc.StreamingConnectionIdleTimeout, + kc.Recorder, + kc.CadvisorInterface, + kc.ImageGCPolicy, + kc.DiskSpacePolicy, + kc.Cloud, + kc.NodeStatusUpdateFrequency, + kc.ResourceContainer, + kc.OSInterface, + kc.CgroupRoot, + kc.ContainerRuntime, + kc.Mounter, + kc.DockerDaemonContainer) + + if err != nil { + return nil, nil, err + } + + k.BirthCry() + + k.StartGarbageCollection() + + return k, pc, nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/persistent_volume_claim_binder.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/persistent_volume_claim_binder.go new file mode 100644 index 000000000000..ecfd736d229c --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/persistent_volume_claim_binder.go @@ -0,0 +1,357 @@ +/* +Copyright 2014 The Kubernetes Authors 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 volumeclaimbinder + +import ( + "fmt" + "sync" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/controller/framework" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + + "github.com/golang/glog" +) + +// PersistentVolumeClaimBinder is a controller that synchronizes PersistentVolumeClaims. +type PersistentVolumeClaimBinder struct { + volumeIndex *persistentVolumeOrderedIndex + volumeController *framework.Controller + claimController *framework.Controller + client binderClient + stopChannels map[string]chan struct{} + lock sync.RWMutex +} + +// NewPersistentVolumeClaimBinder creates a new PersistentVolumeClaimBinder +func NewPersistentVolumeClaimBinder(kubeClient client.Interface, syncPeriod time.Duration) *PersistentVolumeClaimBinder { + volumeIndex := NewPersistentVolumeOrderedIndex() + binderClient := NewBinderClient(kubeClient) + binder := &PersistentVolumeClaimBinder{ + volumeIndex: volumeIndex, + client: binderClient, + } + + _, volumeController := framework.NewInformer( + &cache.ListWatch{ + ListFunc: func() (runtime.Object, error) { + return kubeClient.PersistentVolumes().List(labels.Everything(), fields.Everything()) + }, + WatchFunc: func(resourceVersion string) (watch.Interface, error) { + return kubeClient.PersistentVolumes().Watch(labels.Everything(), fields.Everything(), resourceVersion) + }, + }, + &api.PersistentVolume{}, + syncPeriod, + framework.ResourceEventHandlerFuncs{ + AddFunc: binder.addVolume, + UpdateFunc: binder.updateVolume, + DeleteFunc: binder.deleteVolume, + }, + ) + _, claimController := framework.NewInformer( + &cache.ListWatch{ + ListFunc: func() (runtime.Object, error) { + return kubeClient.PersistentVolumeClaims(api.NamespaceAll).List(labels.Everything(), fields.Everything()) + }, + WatchFunc: func(resourceVersion string) (watch.Interface, error) { + return kubeClient.PersistentVolumeClaims(api.NamespaceAll).Watch(labels.Everything(), fields.Everything(), resourceVersion) + }, + }, + &api.PersistentVolumeClaim{}, + syncPeriod, + framework.ResourceEventHandlerFuncs{ + AddFunc: binder.addClaim, + UpdateFunc: binder.updateClaim, + // no DeleteFunc needed. a claim requires no clean-up. + // syncVolume handles the missing claim + }, + ) + + binder.claimController = claimController + binder.volumeController = volumeController + + return binder +} + +func (binder *PersistentVolumeClaimBinder) addVolume(obj interface{}) { + binder.lock.Lock() + defer binder.lock.Unlock() + volume := obj.(*api.PersistentVolume) + syncVolume(binder.volumeIndex, binder.client, volume) +} + +func (binder *PersistentVolumeClaimBinder) updateVolume(oldObj, newObj interface{}) { + binder.lock.Lock() + defer binder.lock.Unlock() + newVolume := newObj.(*api.PersistentVolume) + binder.volumeIndex.Update(newVolume) + syncVolume(binder.volumeIndex, binder.client, newVolume) +} + +func (binder *PersistentVolumeClaimBinder) deleteVolume(obj interface{}) { + binder.lock.Lock() + defer binder.lock.Unlock() + volume := obj.(*api.PersistentVolume) + binder.volumeIndex.Delete(volume) +} + +func (binder *PersistentVolumeClaimBinder) addClaim(obj interface{}) { + binder.lock.Lock() + defer binder.lock.Unlock() + claim := obj.(*api.PersistentVolumeClaim) + syncClaim(binder.volumeIndex, binder.client, claim) +} + +func (binder *PersistentVolumeClaimBinder) updateClaim(oldObj, newObj interface{}) { + binder.lock.Lock() + defer binder.lock.Unlock() + newClaim := newObj.(*api.PersistentVolumeClaim) + syncClaim(binder.volumeIndex, binder.client, newClaim) +} + +func syncVolume(volumeIndex *persistentVolumeOrderedIndex, binderClient binderClient, volume *api.PersistentVolume) (err error) { + glog.V(5).Infof("Synchronizing PersistentVolume[%s]\n", volume.Name) + + // volumes can be in one of the following states: + // + // VolumePending -- default value -- not bound to a claim and not yet processed through this controller. + // VolumeAvailable -- not bound to a claim, but processed at least once and found in this controller's volumeIndex. + // VolumeBound -- bound to a claim because volume.Spec.ClaimRef != nil. Claim status may not be correct. + // VolumeReleased -- volume.Spec.ClaimRef != nil but the claim has been deleted by the user. + currentPhase := volume.Status.Phase + nextPhase := currentPhase + + switch currentPhase { + // pending volumes are available only after indexing in order to be matched to claims. + case api.VolumePending: + _, exists, err := volumeIndex.Get(volume) + if err != nil { + return err + } + if !exists { + volumeIndex.Add(volume) + } + glog.V(5).Infof("PersistentVolume[%s] is now available\n", volume.Name) + nextPhase = api.VolumeAvailable + + // available volumes await a claim + case api.VolumeAvailable: + if volume.Spec.ClaimRef != nil { + _, err := binderClient.GetPersistentVolumeClaim(volume.Spec.ClaimRef.Namespace, volume.Spec.ClaimRef.Name) + if err == nil { + // change of phase will trigger an update event with the newly bound volume + glog.V(5).Infof("PersistentVolume[%s] is now bound\n", volume.Name) + nextPhase = api.VolumeBound + } else { + if errors.IsNotFound(err) { + nextPhase = api.VolumeReleased + } + } + } + //bound volumes require verification of their bound claims + case api.VolumeBound: + if volume.Spec.ClaimRef == nil { + return fmt.Errorf("PersistentVolume[%s] expected to be bound but found nil claimRef: %+v", volume) + } else { + _, err := binderClient.GetPersistentVolumeClaim(volume.Spec.ClaimRef.Namespace, volume.Spec.ClaimRef.Name) + if err != nil { + if errors.IsNotFound(err) { + nextPhase = api.VolumeReleased + } else { + return err + } + } + } + // released volumes require recycling + case api.VolumeReleased: + if volume.Spec.ClaimRef == nil { + return fmt.Errorf("PersistentVolume[%s] expected to be bound but found nil claimRef: %+v", volume) + } else { + // TODO: implement Recycle method on plugins + } + } + + if currentPhase != nextPhase { + volume.Status.Phase = nextPhase + + // a change in state will trigger another update through this controller. + // each pass through this controller evaluates current phase and decides whether or not to change to the next phase + volume, err := binderClient.UpdatePersistentVolumeStatus(volume) + if err != nil { + // Rollback to previous phase + volume.Status.Phase = currentPhase + } + volumeIndex.Update(volume) + } + + return nil +} + +func syncClaim(volumeIndex *persistentVolumeOrderedIndex, binderClient binderClient, claim *api.PersistentVolumeClaim) (err error) { + glog.V(5).Infof("Synchronizing PersistentVolumeClaim[%s]\n", claim.Name) + + // claims can be in one of the following states: + // + // ClaimPending -- default value -- not bound to a claim. A volume that matches the claim may not exist. + // ClaimBound -- bound to a volume. claim.Status.VolumeRef != nil + currentPhase := claim.Status.Phase + nextPhase := currentPhase + + switch currentPhase { + // pending claims await a matching volume + case api.ClaimPending: + volume, err := volumeIndex.FindBestMatchForClaim(claim) + if err != nil { + return err + } + if volume == nil { + return fmt.Errorf("A volume match does not exist for persistent claim: %s", claim.Name) + } + + // make a binding reference to the claim. + // triggers update of the claim in this controller, which builds claim status + claim.Spec.VolumeName = volume.Name + // TODO: make this similar to Pod's binding both with BindingREST subresource and GuaranteedUpdate helper in etcd.go + claim, err = binderClient.UpdatePersistentVolumeClaim(claim) + if err == nil { + nextPhase = api.ClaimBound + glog.V(5).Infof("PersistentVolumeClaim[%s] is bound\n", claim.Name) + } else { + // Rollback by unsetting the ClaimRef on the volume pointer. + // the volume in the index will be unbound again and ready to be matched. + claim.Spec.VolumeName = "" + // Rollback by restoring original phase to claim pointer + nextPhase = api.ClaimPending + return fmt.Errorf("Error updating volume: %+v\n", err) + } + + case api.ClaimBound: + volume, err := binderClient.GetPersistentVolume(claim.Spec.VolumeName) + if err != nil { + return fmt.Errorf("Unexpected error getting persistent volume: %v\n", err) + } + + if volume.Spec.ClaimRef == nil { + claimRef, err := api.GetReference(claim) + if err != nil { + return fmt.Errorf("Unexpected error getting claim reference: %v\n", err) + } + volume.Spec.ClaimRef = claimRef + _, err = binderClient.UpdatePersistentVolume(volume) + if err != nil { + return fmt.Errorf("Unexpected error saving PersistentVolume.Status: %+v", err) + } + } + + // all "actuals" are transferred from PV to PVC so the user knows what + // type of volume they actually got for their claim. + // Volumes cannot have zero AccessModes, so checking that a claim has access modes + // is sufficient to tell us if these values have already been set. + if len(claim.Status.AccessModes) == 0 { + claim.Status.Phase = api.ClaimBound + claim.Status.AccessModes = volume.Spec.AccessModes + claim.Status.Capacity = volume.Spec.Capacity + _, err := binderClient.UpdatePersistentVolumeClaimStatus(claim) + if err != nil { + return fmt.Errorf("Unexpected error saving claim status: %+v", err) + } + } + } + + if currentPhase != nextPhase { + claim.Status.Phase = nextPhase + binderClient.UpdatePersistentVolumeClaimStatus(claim) + } + return nil +} + +// Run starts all of this binder's control loops +func (controller *PersistentVolumeClaimBinder) Run() { + glog.V(5).Infof("Starting PersistentVolumeClaimBinder\n") + if controller.stopChannels == nil { + controller.stopChannels = make(map[string]chan struct{}) + } + + if _, exists := controller.stopChannels["volumes"]; !exists { + controller.stopChannels["volumes"] = make(chan struct{}) + go controller.volumeController.Run(controller.stopChannels["volumes"]) + } + + if _, exists := controller.stopChannels["claims"]; !exists { + controller.stopChannels["claims"] = make(chan struct{}) + go controller.claimController.Run(controller.stopChannels["claims"]) + } +} + +// Stop gracefully shuts down this binder +func (controller *PersistentVolumeClaimBinder) Stop() { + glog.V(5).Infof("Stopping PersistentVolumeClaimBinder\n") + for name, stopChan := range controller.stopChannels { + close(stopChan) + delete(controller.stopChannels, name) + } +} + +// binderClient abstracts access to PVs and PVCs +type binderClient interface { + GetPersistentVolume(name string) (*api.PersistentVolume, error) + UpdatePersistentVolume(volume *api.PersistentVolume) (*api.PersistentVolume, error) + UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error) + GetPersistentVolumeClaim(namespace, name string) (*api.PersistentVolumeClaim, error) + UpdatePersistentVolumeClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) + UpdatePersistentVolumeClaimStatus(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) +} + +func NewBinderClient(c client.Interface) binderClient { + return &realBinderClient{c} +} + +type realBinderClient struct { + client client.Interface +} + +func (c *realBinderClient) GetPersistentVolume(name string) (*api.PersistentVolume, error) { + return c.client.PersistentVolumes().Get(name) +} + +func (c *realBinderClient) UpdatePersistentVolume(volume *api.PersistentVolume) (*api.PersistentVolume, error) { + return c.client.PersistentVolumes().Update(volume) +} + +func (c *realBinderClient) UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error) { + return c.client.PersistentVolumes().UpdateStatus(volume) +} + +func (c *realBinderClient) GetPersistentVolumeClaim(namespace, name string) (*api.PersistentVolumeClaim, error) { + return c.client.PersistentVolumeClaims(namespace).Get(name) +} + +func (c *realBinderClient) UpdatePersistentVolumeClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) { + return c.client.PersistentVolumeClaims(claim.Namespace).Update(claim) +} + +func (c *realBinderClient) UpdatePersistentVolumeClaimStatus(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) { + return c.client.PersistentVolumeClaims(claim.Namespace).UpdateStatus(claim) +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/persistent_volume_claim_binder_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/persistent_volume_claim_binder_test.go new file mode 100644 index 000000000000..42cac5778baa --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/persistent_volume_claim_binder_test.go @@ -0,0 +1,268 @@ +/* +Copyright 2014 The Kubernetes Authors 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 volumeclaimbinder + +import ( + "reflect" + "testing" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" +) + +func TestRunStop(t *testing.T) { + o := testclient.NewObjects(api.Scheme, api.Scheme) + client := &testclient.Fake{ReactFn: testclient.ObjectReaction(o, latest.RESTMapper)} + binder := NewPersistentVolumeClaimBinder(client, 1*time.Second) + + if len(binder.stopChannels) != 0 { + t.Errorf("Non-running binder should not have any stopChannels. Got %v", len(binder.stopChannels)) + } + + binder.Run() + + if len(binder.stopChannels) != 2 { + t.Errorf("Running binder should have exactly 2 stopChannels. Got %v", len(binder.stopChannels)) + } + + binder.Stop() + + if len(binder.stopChannels) != 0 { + t.Errorf("Non-running binder should not have any stopChannels. Got %v", len(binder.stopChannels)) + } +} + +func TestExampleObjects(t *testing.T) { + scenarios := map[string]struct { + expected interface{} + }{ + "claims/claim-01.yaml": { + expected: &api.PersistentVolumeClaim{ + Spec: api.PersistentVolumeClaimSpec{ + AccessModes: []api.AccessModeType{api.ReadWriteOnce}, + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("3Gi"), + }, + }, + }, + }, + }, + "claims/claim-02.yaml": { + expected: &api.PersistentVolumeClaim{ + Spec: api.PersistentVolumeClaimSpec{ + AccessModes: []api.AccessModeType{api.ReadWriteOnce}, + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("8Gi"), + }, + }, + }, + }, + }, + "volumes/local-01.yaml": { + expected: &api.PersistentVolume{ + Spec: api.PersistentVolumeSpec{ + AccessModes: []api.AccessModeType{api.ReadWriteOnce}, + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("10Gi"), + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + HostPath: &api.HostPathVolumeSource{ + Path: "/tmp/data01", + }, + }, + }, + }, + }, + "volumes/local-02.yaml": { + expected: &api.PersistentVolume{ + Spec: api.PersistentVolumeSpec{ + AccessModes: []api.AccessModeType{api.ReadWriteOnce}, + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("5Gi"), + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + HostPath: &api.HostPathVolumeSource{ + Path: "/tmp/data02", + }, + }, + }, + }, + }, + } + + for name, scenario := range scenarios { + o := testclient.NewObjects(api.Scheme, api.Scheme) + if err := testclient.AddObjectsFromPath("../../examples/persistent-volumes/"+name, o, api.Scheme); err != nil { + t.Fatal(err) + } + + client := &testclient.Fake{ReactFn: testclient.ObjectReaction(o, latest.RESTMapper)} + + if reflect.TypeOf(scenario.expected) == reflect.TypeOf(&api.PersistentVolumeClaim{}) { + pvc, err := client.PersistentVolumeClaims("ns").Get("doesntmatter") + if err != nil { + t.Errorf("Error retrieving object: %v", err) + } + + expected := scenario.expected.(*api.PersistentVolumeClaim) + if pvc.Spec.AccessModes[0] != expected.Spec.AccessModes[0] { + t.Errorf("Unexpected mismatch. Got %v wanted %v", pvc.Spec.AccessModes[0], expected.Spec.AccessModes[0]) + } + + aQty := pvc.Spec.Resources.Requests[api.ResourceStorage] + bQty := expected.Spec.Resources.Requests[api.ResourceStorage] + aSize := aQty.Value() + bSize := bQty.Value() + + if aSize != bSize { + t.Errorf("Unexpected mismatch. Got %v wanted %v", aSize, bSize) + } + } + + if reflect.TypeOf(scenario.expected) == reflect.TypeOf(&api.PersistentVolume{}) { + pv, err := client.PersistentVolumes().Get("doesntmatter") + if err != nil { + t.Errorf("Error retrieving object: %v", err) + } + + expected := scenario.expected.(*api.PersistentVolume) + if pv.Spec.AccessModes[0] != expected.Spec.AccessModes[0] { + t.Errorf("Unexpected mismatch. Got %v wanted %v", pv.Spec.AccessModes[0], expected.Spec.AccessModes[0]) + } + + aQty := pv.Spec.Capacity[api.ResourceStorage] + bQty := expected.Spec.Capacity[api.ResourceStorage] + aSize := aQty.Value() + bSize := bQty.Value() + + if aSize != bSize { + t.Errorf("Unexpected mismatch. Got %v wanted %v", aSize, bSize) + } + + if pv.Spec.HostPath.Path != expected.Spec.HostPath.Path { + t.Errorf("Unexpected mismatch. Got %v wanted %v", pv.Spec.HostPath.Path, expected.Spec.HostPath.Path) + } + } + } +} + +func TestBindingWithExamples(t *testing.T) { + api.ForTesting_ReferencesAllowBlankSelfLinks = true + o := testclient.NewObjects(api.Scheme, api.Scheme) + if err := testclient.AddObjectsFromPath("../../examples/persistent-volumes/claims/claim-01.yaml", o, api.Scheme); err != nil { + t.Fatal(err) + } + if err := testclient.AddObjectsFromPath("../../examples/persistent-volumes/volumes/local-01.yaml", o, api.Scheme); err != nil { + t.Fatal(err) + } + + client := &testclient.Fake{ReactFn: testclient.ObjectReaction(o, latest.RESTMapper)} + + pv, err := client.PersistentVolumes().Get("any") + if err != nil { + t.Error("Unexpected error getting PV from client: %v", err) + } + + claim, error := client.PersistentVolumeClaims("ns").Get("any") + if error != nil { + t.Errorf("Unexpected error getting PVC from client: %v", err) + } + + volumeIndex := NewPersistentVolumeOrderedIndex() + mockClient := &mockBinderClient{ + volume: pv, + claim: claim, + } + + // adds the volume to the index, making the volume available + syncVolume(volumeIndex, mockClient, pv) + if pv.Status.Phase != api.VolumeAvailable { + t.Errorf("Expected phase %s but got %s", api.VolumeBound, pv.Status.Phase) + } + + // an initial sync for a claim will bind it to an unbound volume, triggers state change + syncClaim(volumeIndex, mockClient, claim) + // state change causes another syncClaim to update statuses + syncClaim(volumeIndex, mockClient, claim) + // claim updated volume's status, causing an update and syncVolume call + syncVolume(volumeIndex, mockClient, pv) + + if pv.Spec.ClaimRef == nil { + t.Errorf("Expected ClaimRef but got nil for pv.Status.ClaimRef: %+v\n", pv) + } + + if pv.Status.Phase != api.VolumeBound { + t.Errorf("Expected phase %s but got %s", api.VolumeBound, pv.Status.Phase) + } + + if claim.Status.Phase != api.ClaimBound { + t.Errorf("Expected phase %s but got %s", api.ClaimBound, claim.Status.Phase) + } + if len(claim.Status.AccessModes) != len(pv.Spec.AccessModes) { + t.Errorf("Expected phase %s but got %s", api.ClaimBound, claim.Status.Phase) + } + if claim.Status.AccessModes[0] != pv.Spec.AccessModes[0] { + t.Errorf("Expected access mode %s but got %s", claim.Status.AccessModes[0], pv.Spec.AccessModes[0]) + } + + // pretend the user deleted their claim + mockClient.claim = nil + syncVolume(volumeIndex, mockClient, pv) + + if pv.Status.Phase != api.VolumeReleased { + t.Errorf("Expected phase %s but got %s", api.VolumeReleased, pv.Status.Phase) + } +} + +type mockBinderClient struct { + volume *api.PersistentVolume + claim *api.PersistentVolumeClaim +} + +func (c *mockBinderClient) GetPersistentVolume(name string) (*api.PersistentVolume, error) { + return c.volume, nil +} + +func (c *mockBinderClient) UpdatePersistentVolume(volume *api.PersistentVolume) (*api.PersistentVolume, error) { + return volume, nil +} + +func (c *mockBinderClient) UpdatePersistentVolumeStatus(volume *api.PersistentVolume) (*api.PersistentVolume, error) { + return volume, nil +} + +func (c *mockBinderClient) GetPersistentVolumeClaim(namespace, name string) (*api.PersistentVolumeClaim, error) { + if c.claim != nil { + return c.claim, nil + } else { + return nil, errors.NewNotFound("persistentVolume", name) + } +} + +func (c *mockBinderClient) UpdatePersistentVolumeClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) { + return claim, nil +} + +func (c *mockBinderClient) UpdatePersistentVolumeClaimStatus(claim *api.PersistentVolumeClaim) (*api.PersistentVolumeClaim, error) { + return claim, nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/persistent_volume_index_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/persistent_volume_index_test.go new file mode 100644 index 000000000000..8a7d6f4c7a96 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/persistent_volume_index_test.go @@ -0,0 +1,269 @@ +/* +Copyright 2014 The Kubernetes Authors 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 volumeclaimbinder + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" +) + +func TestMatchVolume(t *testing.T) { + volList := NewPersistentVolumeOrderedIndex() + for _, pv := range createTestVolumes() { + volList.Add(pv) + } + + scenarios := map[string]struct { + expectedMatch string + claim *api.PersistentVolumeClaim + }{ + "successful-match-gce-10": { + expectedMatch: "gce-pd-10", + claim: &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{ + Name: "claim01", + Namespace: "myns", + }, + Spec: api.PersistentVolumeClaimSpec{ + AccessModes: []api.AccessModeType{api.ReadOnlyMany, api.ReadWriteOnce}, + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("8G"), + }, + }, + }, + }, + }, + "successful-match-nfs-5": { + expectedMatch: "nfs-5", + claim: &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{ + Name: "claim01", + Namespace: "myns", + }, + Spec: api.PersistentVolumeClaimSpec{ + AccessModes: []api.AccessModeType{api.ReadOnlyMany, api.ReadWriteOnce, api.ReadWriteMany}, + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("5G"), + }, + }, + }, + }, + }, + "successful-skip-1g-bound-volume": { + expectedMatch: "gce-pd-5", + claim: &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{ + Name: "claim01", + Namespace: "myns", + }, + Spec: api.PersistentVolumeClaimSpec{ + AccessModes: []api.AccessModeType{api.ReadOnlyMany, api.ReadWriteOnce}, + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("1G"), + }, + }, + }, + }, + }, + "successful-no-match": { + expectedMatch: "", + claim: &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{ + Name: "claim01", + Namespace: "myns", + }, + Spec: api.PersistentVolumeClaimSpec{ + AccessModes: []api.AccessModeType{api.ReadOnlyMany, api.ReadWriteOnce}, + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("999G"), + }, + }, + }, + }, + }, + } + + for name, scenario := range scenarios { + volume, err := volList.FindBestMatchForClaim(scenario.claim) + if err != nil { + t.Errorf("Unexpected error matching volume by claim: %v", err) + } + if scenario.expectedMatch != "" && volume == nil { + t.Errorf("Expected match but received nil volume for scenario: %s", name) + } + if scenario.expectedMatch != "" && volume != nil && string(volume.UID) != scenario.expectedMatch { + t.Errorf("Expected %s but got volume %s instead", scenario.expectedMatch, volume.UID) + } + if scenario.expectedMatch == "" && volume != nil { + t.Errorf("Unexpected match for scenario: %s", name) + } + } +} + +func TestSort(t *testing.T) { + volList := NewPersistentVolumeOrderedIndex() + for _, pv := range createTestVolumes() { + volList.Add(pv) + } + + volumes, err := volList.ListByAccessModes([]api.AccessModeType{api.ReadWriteOnce, api.ReadOnlyMany}) + if err != nil { + t.Error("Unexpected error retrieving volumes by access modes:", err) + } + + for i, expected := range []string{"gce-pd-1", "gce-pd-5", "gce-pd-10"} { + if string(volumes[i].UID) != expected { + t.Error("Incorrect ordering of persistent volumes. Expected %s but got %s", expected, volumes[i].UID) + } + } + + volumes, err = volList.ListByAccessModes([]api.AccessModeType{api.ReadWriteOnce, api.ReadOnlyMany, api.ReadWriteMany}) + if err != nil { + t.Error("Unexpected error retrieving volumes by access modes:", err) + } + + for i, expected := range []string{"nfs-1", "nfs-5", "nfs-10"} { + if string(volumes[i].UID) != expected { + t.Error("Incorrect ordering of persistent volumes. Expected %s but got %s", expected, volumes[i].UID) + } + } +} + +func createTestVolumes() []*api.PersistentVolume { + // these volumes are deliberately out-of-order to test indexing and sorting + return []*api.PersistentVolume{ + { + ObjectMeta: api.ObjectMeta{ + UID: "gce-pd-10", + Name: "gce003", + }, + Spec: api.PersistentVolumeSpec{ + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("10G"), + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{}, + }, + AccessModes: []api.AccessModeType{ + api.ReadWriteOnce, + api.ReadOnlyMany, + }, + }, + }, + { + ObjectMeta: api.ObjectMeta{ + UID: "nfs-5", + Name: "nfs002", + }, + Spec: api.PersistentVolumeSpec{ + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("5G"), + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + Glusterfs: &api.GlusterfsVolumeSource{}, + }, + AccessModes: []api.AccessModeType{ + api.ReadWriteOnce, + api.ReadOnlyMany, + api.ReadWriteMany, + }, + }, + }, + { + ObjectMeta: api.ObjectMeta{ + UID: "gce-pd-1", + Name: "gce001", + }, + Spec: api.PersistentVolumeSpec{ + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("1G"), + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{}, + }, + AccessModes: []api.AccessModeType{ + api.ReadWriteOnce, + api.ReadOnlyMany, + }, + // this one we're pretending is already bound + ClaimRef: &api.ObjectReference{UID: "abc123"}, + }, + }, + { + ObjectMeta: api.ObjectMeta{ + UID: "nfs-10", + Name: "nfs003", + }, + Spec: api.PersistentVolumeSpec{ + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("10G"), + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + Glusterfs: &api.GlusterfsVolumeSource{}, + }, + AccessModes: []api.AccessModeType{ + api.ReadWriteOnce, + api.ReadOnlyMany, + api.ReadWriteMany, + }, + }, + }, + { + ObjectMeta: api.ObjectMeta{ + UID: "gce-pd-5", + Name: "gce002", + }, + Spec: api.PersistentVolumeSpec{ + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("5G"), + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{}, + }, + AccessModes: []api.AccessModeType{ + api.ReadWriteOnce, + api.ReadOnlyMany, + }, + }, + }, + { + ObjectMeta: api.ObjectMeta{ + UID: "nfs-1", + Name: "nfs001", + }, + Spec: api.PersistentVolumeSpec{ + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse("1G"), + }, + PersistentVolumeSource: api.PersistentVolumeSource{ + Glusterfs: &api.GlusterfsVolumeSource{}, + }, + AccessModes: []api.AccessModeType{ + api.ReadWriteOnce, + api.ReadOnlyMany, + api.ReadWriteMany, + }, + }, + }, + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/types.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/types.go new file mode 100644 index 000000000000..b48dd71f30b9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder/types.go @@ -0,0 +1,146 @@ +/* +Copyright 2014 The Kubernetes Authors 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 volumeclaimbinder + +import ( + "fmt" + "sort" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" +) + +// persistentVolumeOrderedIndex is a cache.Store that keeps persistent volumes indexed by AccessModes and ordered by storage capacity. +type persistentVolumeOrderedIndex struct { + cache.Indexer +} + +var _ cache.Store = &persistentVolumeOrderedIndex{} // persistentVolumeOrderedIndex is a Store + +func NewPersistentVolumeOrderedIndex() *persistentVolumeOrderedIndex { + return &persistentVolumeOrderedIndex{ + cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"accessmodes": accessModesIndexFunc}), + } +} + +// accessModesIndexFunc is an indexing function that returns a persistent volume's AccessModes as a string +func accessModesIndexFunc(obj interface{}) (string, error) { + if pv, ok := obj.(*api.PersistentVolume); ok { + modes := volume.GetAccessModesAsString(pv.Spec.AccessModes) + return modes, nil + } + return "", fmt.Errorf("object is not a persistent volume: %v", obj) +} + +// ListByAccessModes returns all volumes with the given set of AccessModeTypes *in order* of their storage capacity (low to high) +func (pvIndex *persistentVolumeOrderedIndex) ListByAccessModes(modes []api.AccessModeType) ([]*api.PersistentVolume, error) { + pv := &api.PersistentVolume{ + Spec: api.PersistentVolumeSpec{ + AccessModes: modes, + }, + } + + objs, err := pvIndex.Index("accessmodes", pv) + if err != nil { + return nil, err + } + + volumes := make([]*api.PersistentVolume, len(objs)) + for i, obj := range objs { + volumes[i] = obj.(*api.PersistentVolume) + } + + sort.Sort(byCapacity{volumes}) + return volumes, nil +} + +// matchPredicate is a function that indicates that a persistent volume matches another +type matchPredicate func(compareThis, toThis *api.PersistentVolume) bool + +// Find returns the nearest PV from the ordered list or nil if a match is not found +func (pvIndex *persistentVolumeOrderedIndex) Find(pv *api.PersistentVolume, matchPredicate matchPredicate) (*api.PersistentVolume, error) { + volumes, err := pvIndex.ListByAccessModes(pv.Spec.AccessModes) + if err != nil { + return nil, err + } + + i := sort.Search(len(volumes), func(i int) bool { return matchPredicate(pv, volumes[i]) }) + if i < len(volumes) { + return volumes[i], nil + } + return nil, nil +} + +// FindByAccessModesAndStorageCapacity is a convenience method that calls Find w/ requisite matchPredicate for storage +func (pvIndex *persistentVolumeOrderedIndex) FindByAccessModesAndStorageCapacity(modes []api.AccessModeType, qty resource.Quantity) (*api.PersistentVolume, error) { + pv := &api.PersistentVolume{ + Spec: api.PersistentVolumeSpec{ + AccessModes: modes, + Capacity: api.ResourceList{ + api.ResourceName(api.ResourceStorage): qty, + }, + }, + } + + return pvIndex.Find(pv, filterBoundVolumes) +} + +// FindBestMatchForClaim is a convenience method that finds a volume by the claim's AccessModes and requests for Storage +func (pvIndex *persistentVolumeOrderedIndex) FindBestMatchForClaim(claim *api.PersistentVolumeClaim) (*api.PersistentVolume, error) { + return pvIndex.FindByAccessModesAndStorageCapacity(claim.Spec.AccessModes, claim.Spec.Resources.Requests[api.ResourceName(api.ResourceStorage)]) +} + +// byCapacity is used to order volumes by ascending storage size +type byCapacity struct { + volumes []*api.PersistentVolume +} + +func (c byCapacity) Less(i, j int) bool { + return matchStorageCapacity(c.volumes[i], c.volumes[j]) +} + +func (c byCapacity) Swap(i, j int) { + c.volumes[i], c.volumes[j] = c.volumes[j], c.volumes[i] +} + +func (c byCapacity) Len() int { + return len(c.volumes) +} + +// matchStorageCapacity is a matchPredicate used to sort and find volumes +func matchStorageCapacity(pvA, pvB *api.PersistentVolume) bool { + // skip already claimed volumes + if pvA.Spec.ClaimRef != nil { + return false + } + + aQty := pvA.Spec.Capacity[api.ResourceStorage] + bQty := pvB.Spec.Capacity[api.ResourceStorage] + aSize := aQty.Value() + bSize := bQty.Value() + return aSize <= bSize +} + +// filterBoundVolumes is a matchPredicate that filters bound volumes before comparing storage capacity +func filterBoundVolumes(compareThis, toThis *api.PersistentVolume) bool { + if compareThis.Spec.ClaimRef != nil || toThis.Spec.ClaimRef != nil { + return false + } + return matchStorageCapacity(compareThis, toThis) +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/cmd/kube-scheduler/app/server.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/cmd/kube-scheduler/app/server.go new file mode 100644 index 000000000000..e2cca460b570 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/cmd/kube-scheduler/app/server.go @@ -0,0 +1,160 @@ +/* +Copyright 2014 The Kubernetes Authors 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 app implements a Server object for running the scheduler. +package app + +import ( + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/pprof" + "os" + "strconv" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" + "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" + "github.com/GoogleCloudPlatform/kubernetes/pkg/master/ports" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/algorithmprovider" + schedulerapi "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/api" + latestschedulerapi "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/factory" + + "github.com/golang/glog" + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/pflag" +) + +// SchedulerServer has all the context and params needed to run a Scheduler +type SchedulerServer struct { + Port int + Address util.IP + AlgorithmProvider string + PolicyConfigFile string + EnableProfiling bool + Master string + Kubeconfig string +} + +// NewSchedulerServer creates a new SchedulerServer with default parameters +func NewSchedulerServer() *SchedulerServer { + s := SchedulerServer{ + Port: ports.SchedulerPort, + Address: util.IP(net.ParseIP("127.0.0.1")), + AlgorithmProvider: factory.DefaultProvider, + } + return &s +} + +// AddFlags adds flags for a specific SchedulerServer to the specified FlagSet +func (s *SchedulerServer) AddFlags(fs *pflag.FlagSet) { + fs.IntVar(&s.Port, "port", s.Port, "The port that the scheduler's http service runs on") + fs.Var(&s.Address, "address", "The IP address to serve on (set to 0.0.0.0 for all interfaces)") + fs.StringVar(&s.AlgorithmProvider, "algorithm-provider", s.AlgorithmProvider, "The scheduling algorithm provider to use, one of: "+factory.ListAlgorithmProviders()) + fs.StringVar(&s.PolicyConfigFile, "policy-config-file", s.PolicyConfigFile, "File with scheduler policy configuration") + fs.BoolVar(&s.EnableProfiling, "profiling", true, "Enable profiling via web interface host:port/debug/pprof/") + fs.StringVar(&s.Master, "master", s.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig)") + fs.StringVar(&s.Kubeconfig, "kubeconfig", s.Kubeconfig, "Path to kubeconfig file with authorization and master location information.") +} + +// Run runs the specified SchedulerServer. This should never exit. +func (s *SchedulerServer) Run(_ []string) error { + if s.Kubeconfig == "" && s.Master == "" { + glog.Warningf("Neither --kubeconfig nor --master was specified. Using default API client. This might not work.") + } + + // This creates a client, first loading any specified kubeconfig + // file, and then overriding the Master flag, if non-empty. + kubeconfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: s.Kubeconfig}, + &clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: s.Master}}).ClientConfig() + if err != nil { + return err + } + kubeconfig.QPS = 20.0 + kubeconfig.Burst = 100 + + kubeClient, err := client.New(kubeconfig) + if err != nil { + glog.Fatalf("Invalid API configuration: %v", err) + } + + go func() { + mux := http.NewServeMux() + healthz.InstallHandler(mux) + if s.EnableProfiling { + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + } + mux.Handle("/metrics", prometheus.Handler()) + + server := &http.Server{ + Addr: net.JoinHostPort(s.Address.String(), strconv.Itoa(s.Port)), + Handler: mux, + } + glog.Fatal(server.ListenAndServe()) + }() + + configFactory := factory.NewConfigFactory(kubeClient) + config, err := s.createConfig(configFactory) + if err != nil { + glog.Fatalf("Failed to create scheduler configuration: %v", err) + } + + eventBroadcaster := record.NewBroadcaster() + config.Recorder = eventBroadcaster.NewRecorder(api.EventSource{Component: "scheduler"}) + eventBroadcaster.StartRecordingToSink(kubeClient.Events("")) + + sched := scheduler.New(config) + sched.Run() + + select {} +} + +func (s *SchedulerServer) createConfig(configFactory *factory.ConfigFactory) (*scheduler.Config, error) { + var policy schedulerapi.Policy + var configData []byte + + if _, err := os.Stat(s.PolicyConfigFile); err == nil { + configData, err = ioutil.ReadFile(s.PolicyConfigFile) + if err != nil { + return nil, fmt.Errorf("Unable to read policy config: %v", err) + } + err = latestschedulerapi.Codec.DecodeInto(configData, &policy) + if err != nil { + return nil, fmt.Errorf("Invalid configuration: %v", err) + } + + return configFactory.CreateFromConfig(policy) + } + + // if the config file isn't provided, use the specified (or default) provider + // check of algorithm provider is registered and fail fast + _, err := factory.GetAlgorithmProvider(s.AlgorithmProvider) + if err != nil { + return nil, err + } + + return configFactory.CreateFromProvider(s.AlgorithmProvider) +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny/admission.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny/admission.go new file mode 100644 index 000000000000..2fee60f7bdfa --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny/admission.go @@ -0,0 +1,43 @@ +/* +Copyright 2014 The Kubernetes Authors 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 deny + +import ( + "errors" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func init() { + admission.RegisterPlugin("AlwaysDeny", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewAlwaysDeny(), nil + }) +} + +// alwaysDeny is an implementation of admission.Interface which always says no to an admission request. +// It is useful in unit tests to force an operation to be forbidden. +type alwaysDeny struct{} + +func (alwaysDeny) Admit(a admission.Attributes) (err error) { + return admission.NewForbidden(a, errors.New("Admission control is denying all modifications")) +} + +func NewAlwaysDeny() admission.Interface { + return new(alwaysDeny) +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny/admission_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny/admission_test.go new file mode 100644 index 000000000000..3bd2ed91e44f --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny/admission_test.go @@ -0,0 +1,31 @@ +/* +Copyright 2014 The Kubernetes Authors 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 deny + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" +) + +func TestAdmission(t *testing.T) { + handler := NewAlwaysDeny() + err := handler.Admit(admission.NewAttributesRecord(nil, "Pod", "foo", "Pod", "ignored")) + if err == nil { + t.Errorf("Expected error returned from admission handler") + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/autoprovision/admission.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/autoprovision/admission.go new file mode 100644 index 000000000000..dd4bc3cd9302 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/autoprovision/admission.go @@ -0,0 +1,106 @@ +/* +Copyright 2014 The Kubernetes Authors 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 autoprovision + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +func init() { + admission.RegisterPlugin("NamespaceAutoProvision", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewProvision(client), nil + }) +} + +// provision is an implementation of admission.Interface. +// It looks at all incoming requests in a namespace context, and if the namespace does not exist, it creates one. +// It is useful in deployments that do not want to restrict creation of a namespace prior to its usage. +type provision struct { + client client.Interface + store cache.Store +} + +func (p *provision) Admit(a admission.Attributes) (err error) { + // only handle create requests + if a.GetOperation() != "CREATE" { + return nil + } + defaultVersion, kind, err := latest.RESTMapper.VersionAndKindForResource(a.GetResource()) + if err != nil { + return admission.NewForbidden(a, err) + } + mapping, err := latest.RESTMapper.RESTMapping(kind, defaultVersion) + if err != nil { + return admission.NewForbidden(a, err) + } + if mapping.Scope.Name() != meta.RESTScopeNameNamespace { + return nil + } + namespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: a.GetNamespace(), + Namespace: "", + }, + Status: api.NamespaceStatus{}, + } + _, exists, err := p.store.Get(namespace) + if err != nil { + return admission.NewForbidden(a, err) + } + if exists { + return nil + } + _, err = p.client.Namespaces().Create(namespace) + if err != nil && !errors.IsAlreadyExists(err) { + return admission.NewForbidden(a, err) + } + return nil +} + +func NewProvision(c client.Interface) admission.Interface { + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + reflector := cache.NewReflector( + &cache.ListWatch{ + ListFunc: func() (runtime.Object, error) { + return c.Namespaces().List(labels.Everything(), fields.Everything()) + }, + WatchFunc: func(resourceVersion string) (watch.Interface, error) { + return c.Namespaces().Watch(labels.Everything(), fields.Everything(), resourceVersion) + }, + }, + &api.Namespace{}, + store, + 0, + ) + reflector.Run() + return &provision{ + client: c, + store: store, + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/autoprovision/admission_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/autoprovision/admission_test.go new file mode 100644 index 000000000000..6260eeb40230 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/autoprovision/admission_test.go @@ -0,0 +1,130 @@ +/* +Copyright 2014 The Kubernetes Authors 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 autoprovision + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" +) + +// TestAdmission verifies a namespace is created on create requests for namespace managed resources +func TestAdmission(t *testing.T) { + namespace := "test" + mockClient := &testclient.Fake{} + handler := &provision{ + client: mockClient, + store: cache.NewStore(cache.MetaNamespaceKeyFunc), + } + pod := api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image"}}, + }, + } + err := handler.Admit(admission.NewAttributesRecord(&pod, "Pod", namespace, "pods", "CREATE")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler") + } + if len(mockClient.Actions) != 1 { + t.Errorf("Expected a create-namespace request") + } + if mockClient.Actions[0].Action != "create-namespace" { + t.Errorf("Expected a create-namespace request to be made via the client") + } +} + +// TestAdmissionNamespaceExists verifies that no client call is made when a namespace already exists +func TestAdmissionNamespaceExists(t *testing.T) { + namespace := "test" + mockClient := &testclient.Fake{} + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + store.Add(&api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: namespace}, + }) + handler := &provision{ + client: mockClient, + store: store, + } + pod := api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image"}}, + }, + } + err := handler.Admit(admission.NewAttributesRecord(&pod, "Pod", namespace, "pods", "CREATE")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler") + } + if len(mockClient.Actions) != 0 { + t.Errorf("No client request should have been made") + } +} + +// TestIgnoreAdmission validates that a request is ignored if its not a create +func TestIgnoreAdmission(t *testing.T) { + namespace := "test" + mockClient := &testclient.Fake{} + handler := &provision{ + client: mockClient, + store: cache.NewStore(cache.MetaNamespaceKeyFunc), + } + pod := api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image"}}, + }, + } + err := handler.Admit(admission.NewAttributesRecord(&pod, "Pod", namespace, "pods", "UPDATE")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler") + } + if len(mockClient.Actions) != 0 { + t.Errorf("No client request should have been made") + } +} + +// TestAdmissionNamespaceExistsUnknownToHandler +func TestAdmissionNamespaceExistsUnknownToHandler(t *testing.T) { + namespace := "test" + mockClient := &testclient.Fake{ + Err: errors.NewAlreadyExists("namespaces", namespace), + } + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + handler := &provision{ + client: mockClient, + store: store, + } + pod := api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image"}}, + }, + } + err := handler.Admit(admission.NewAttributesRecord(&pod, "Pod", namespace, "pods", "CREATE")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler") + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny/admission.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny/admission.go new file mode 100644 index 000000000000..2362ae3fe5df --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny/admission.go @@ -0,0 +1,70 @@ +/* +Copyright 2014 The Kubernetes Authors 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 scdeny + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func init() { + admission.RegisterPlugin("SecurityContextDeny", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewSecurityContextDeny(client), nil + }) +} + +// plugin contains the client used by the SecurityContextDeny admission controller +type plugin struct { + client client.Interface +} + +// NewSecurityContextDeny creates a new instance of the SecurityContextDeny admission controller +func NewSecurityContextDeny(client client.Interface) admission.Interface { + return &plugin{client} +} + +// Admit will deny any SecurityContext that defines options that were not previously available in the api.Container +// struct (Capabilities and Privileged) +func (p *plugin) Admit(a admission.Attributes) (err error) { + if a.GetOperation() == "DELETE" { + return nil + } + if a.GetResource() != string(api.ResourcePods) { + return nil + } + + pod, ok := a.GetObject().(*api.Pod) + if !ok { + return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") + } + for _, v := range pod.Spec.Containers { + if v.SecurityContext != nil { + if v.SecurityContext.SELinuxOptions != nil { + return apierrors.NewForbidden(a.GetResource(), pod.Name, fmt.Errorf("SecurityContext.SELinuxOptions is forbidden")) + } + if v.SecurityContext.RunAsUser != nil { + return apierrors.NewForbidden(a.GetResource(), pod.Name, fmt.Errorf("SecurityContext.RunAsUser is forbidden")) + } + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny/admission_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny/admission_test.go new file mode 100644 index 000000000000..10c11a1a2864 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny/admission_test.go @@ -0,0 +1,65 @@ +/* +Copyright 2014 The Kubernetes Authors 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 scdeny + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +// ensures the SecurityContext is denied if it defines anything more than Caps or Privileged +func TestAdmission(t *testing.T) { + handler := NewSecurityContextDeny(nil) + + var runAsUser int64 = 1 + priv := true + successCases := map[string]*api.SecurityContext{ + "no sc": nil, + "empty sc": {}, + "valid sc": {Privileged: &priv, Capabilities: &api.Capabilities{}}, + } + + pod := api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{ + {}, + }, + }, + } + for k, v := range successCases { + pod.Spec.Containers[0].SecurityContext = v + err := handler.Admit(admission.NewAttributesRecord(&pod, "Pod", "foo", string(api.ResourcePods), "ignored")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler for case %s", k) + } + } + + errorCases := map[string]*api.SecurityContext{ + "run as user": {RunAsUser: &runAsUser}, + "se linux optons": {SELinuxOptions: &api.SELinuxOptions{}}, + "mixed settings": {Privileged: &priv, RunAsUser: &runAsUser, SELinuxOptions: &api.SELinuxOptions{}}, + } + for k, v := range errorCases { + pod.Spec.Containers[0].SecurityContext = v + err := handler.Admit(admission.NewAttributesRecord(&pod, "Pod", "foo", string(api.ResourcePods), "ignored")) + if err == nil { + t.Errorf("Expected error returned from admission handler for case %s", k) + } + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount/admission.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount/admission.go new file mode 100644 index 000000000000..8d94ba5a6c10 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount/admission.go @@ -0,0 +1,373 @@ +/* +Copyright 2014 The Kubernetes Authors 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 serviceaccount + +import ( + "fmt" + "io" + "math/rand" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// DefaultServiceAccountName is the name of the default service account to set on pods which do not specify a service account +const DefaultServiceAccountName = "default" + +// DefaultAPITokenMountPath is the path that ServiceAccountToken secrets are automounted to. +// The token file would then be accessible at /var/run/secrets/kubernetes.io/serviceaccount +const DefaultAPITokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount" + +func init() { + admission.RegisterPlugin("ServiceAccount", func(client client.Interface, config io.Reader) (admission.Interface, error) { + serviceAccountAdmission := NewServiceAccount(client) + serviceAccountAdmission.Run() + return serviceAccountAdmission, nil + }) +} + +var _ = admission.Interface(&serviceAccount{}) + +type serviceAccount struct { + // LimitSecretReferences rejects pods that reference secrets their service accounts do not reference + LimitSecretReferences bool + // MountServiceAccountToken creates Volume and VolumeMounts for the first referenced ServiceAccountToken for the pod's service account + MountServiceAccountToken bool + + client client.Interface + + serviceAccounts cache.Indexer + secrets cache.Indexer + + stopChan chan struct{} + serviceAccountsReflector *cache.Reflector + secretsReflector *cache.Reflector +} + +// NewServiceAccount returns an admission.Interface implementation which limits admission of Pod CREATE requests based on the pod's ServiceAccount: +// 1. If the pod does not specify a ServiceAccount, it sets the pod's ServiceAccount to "default" +// 2. It ensures the ServiceAccount referenced by the pod exists +// 3. If LimitSecretReferences is true, it rejects the pod if the pod references Secret objects which the pod's ServiceAccount does not reference +// 4. If MountServiceAccountToken is true, it adds a VolumeMount with the pod's ServiceAccount's api token secret to containers +func NewServiceAccount(cl client.Interface) *serviceAccount { + serviceAccountsIndexer, serviceAccountsReflector := cache.NewNamespaceKeyedIndexerAndReflector( + &cache.ListWatch{ + ListFunc: func() (runtime.Object, error) { + return cl.ServiceAccounts(api.NamespaceAll).List(labels.Everything(), fields.Everything()) + }, + WatchFunc: func(resourceVersion string) (watch.Interface, error) { + return cl.ServiceAccounts(api.NamespaceAll).Watch(labels.Everything(), fields.Everything(), resourceVersion) + }, + }, + &api.ServiceAccount{}, + 0, + ) + + tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)}) + secretsIndexer, secretsReflector := cache.NewNamespaceKeyedIndexerAndReflector( + &cache.ListWatch{ + ListFunc: func() (runtime.Object, error) { + return cl.Secrets(api.NamespaceAll).List(labels.Everything(), tokenSelector) + }, + WatchFunc: func(resourceVersion string) (watch.Interface, error) { + return cl.Secrets(api.NamespaceAll).Watch(labels.Everything(), tokenSelector, resourceVersion) + }, + }, + &api.Secret{}, + 0, + ) + + return &serviceAccount{ + // TODO: enable this once we've swept secret usage to account for adding secret references to service accounts + LimitSecretReferences: false, + // Auto mount service account API token secrets + MountServiceAccountToken: true, + + client: cl, + serviceAccounts: serviceAccountsIndexer, + serviceAccountsReflector: serviceAccountsReflector, + secrets: secretsIndexer, + secretsReflector: secretsReflector, + } +} + +func (s *serviceAccount) Run() { + if s.stopChan == nil { + s.stopChan = make(chan struct{}) + s.serviceAccountsReflector.RunUntil(s.stopChan) + s.secretsReflector.RunUntil(s.stopChan) + } +} +func (s *serviceAccount) Stop() { + if s.stopChan != nil { + close(s.stopChan) + s.stopChan = nil + } +} + +func (s *serviceAccount) Admit(a admission.Attributes) (err error) { + // We only care about Pod CREATE operations + if a.GetOperation() != "CREATE" { + return nil + } + if a.GetResource() != string(api.ResourcePods) { + return nil + } + obj := a.GetObject() + if obj == nil { + return nil + } + pod, ok := obj.(*api.Pod) + if !ok { + return nil + } + + // Don't modify the spec of mirror pods. + // That makes the kubelet very angry and confused, and it immediately deletes the pod (because the spec doesn't match) + // That said, don't allow mirror pods to reference ServiceAccounts or SecretVolumeSources either + if _, isMirrorPod := pod.Annotations[kubelet.ConfigMirrorAnnotationKey]; isMirrorPod { + if len(pod.Spec.ServiceAccount) != 0 { + return admission.NewForbidden(a, fmt.Errorf("A mirror pod may not reference service accounts")) + } + for _, volume := range pod.Spec.Volumes { + if volume.VolumeSource.Secret != nil { + return admission.NewForbidden(a, fmt.Errorf("A mirror pod may not reference secrets")) + } + } + return nil + } + + // Set the default service account if needed + if len(pod.Spec.ServiceAccount) == 0 { + pod.Spec.ServiceAccount = DefaultServiceAccountName + } + + // Ensure the referenced service account exists + serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccount) + if err != nil { + return admission.NewForbidden(a, fmt.Errorf("Error looking up service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccount, err)) + } + if serviceAccount == nil { + return admission.NewForbidden(a, fmt.Errorf("Missing service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccount, err)) + } + + if s.LimitSecretReferences { + if err := s.limitSecretReferences(serviceAccount, pod); err != nil { + return admission.NewForbidden(a, err) + } + } + + if s.MountServiceAccountToken { + if err := s.mountServiceAccountToken(serviceAccount, pod); err != nil { + return admission.NewForbidden(a, err) + } + } + + return nil +} + +// getServiceAccount returns the ServiceAccount for the given namespace and name if it exists +func (s *serviceAccount) getServiceAccount(namespace string, name string) (*api.ServiceAccount, error) { + key := &api.ServiceAccount{ObjectMeta: api.ObjectMeta{Namespace: namespace}} + index, err := s.serviceAccounts.Index("namespace", key) + if err != nil { + return nil, err + } + + for _, obj := range index { + serviceAccount := obj.(*api.ServiceAccount) + if serviceAccount.Name == name { + return serviceAccount, nil + } + } + + // Could not find in cache, attempt to look up directly + numAttempts := 1 + if name == DefaultServiceAccountName { + // If this is the default serviceaccount, attempt more times, since it should be auto-created by the controller + numAttempts = 10 + } + retryInterval := time.Duration(rand.Int63n(100)+int64(100)) * time.Millisecond + for i := 0; i < numAttempts; i++ { + if i != 0 { + time.Sleep(retryInterval) + } + serviceAccount, err := s.client.ServiceAccounts(namespace).Get(name) + if err == nil { + return serviceAccount, nil + } + if !errors.IsNotFound(err) { + return nil, err + } + } + + return nil, nil +} + +// getReferencedServiceAccountToken returns the name of the first referenced secret which is a ServiceAccountToken for the service account +func (s *serviceAccount) getReferencedServiceAccountToken(serviceAccount *api.ServiceAccount) (string, error) { + if len(serviceAccount.Secrets) == 0 { + return "", nil + } + + tokens, err := s.getServiceAccountTokens(serviceAccount) + if err != nil { + return "", err + } + + references := util.NewStringSet() + for _, secret := range serviceAccount.Secrets { + references.Insert(secret.Name) + } + for _, token := range tokens { + if references.Has(token.Name) { + return token.Name, nil + } + } + + return "", nil +} + +// getServiceAccountTokens returns all ServiceAccountToken secrets for the given ServiceAccount +func (s *serviceAccount) getServiceAccountTokens(serviceAccount *api.ServiceAccount) ([]*api.Secret, error) { + key := &api.Secret{ObjectMeta: api.ObjectMeta{Namespace: serviceAccount.Namespace}} + index, err := s.secrets.Index("namespace", key) + if err != nil { + return nil, err + } + + tokens := []*api.Secret{} + for _, obj := range index { + token := obj.(*api.Secret) + if token.Type != api.SecretTypeServiceAccountToken { + continue + } + name := token.Annotations[api.ServiceAccountNameKey] + uid := token.Annotations[api.ServiceAccountUIDKey] + if name != serviceAccount.Name { + // Name must match + continue + } + if len(uid) > 0 && uid != string(serviceAccount.UID) { + // If UID is set, it must match + continue + } + tokens = append(tokens, token) + } + return tokens, nil +} + +func (s *serviceAccount) limitSecretReferences(serviceAccount *api.ServiceAccount, pod *api.Pod) error { + // Ensure all secrets the pod references are allowed by the service account + referencedSecrets := util.NewStringSet() + for _, s := range serviceAccount.Secrets { + referencedSecrets.Insert(s.Name) + } + for _, volume := range pod.Spec.Volumes { + source := volume.VolumeSource + if source.Secret == nil { + continue + } + secretName := source.Secret.SecretName + if !referencedSecrets.Has(secretName) { + return fmt.Errorf("Volume with secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", secretName, serviceAccount.Name) + } + } + return nil +} + +func (s *serviceAccount) mountServiceAccountToken(serviceAccount *api.ServiceAccount, pod *api.Pod) error { + // Find the name of a referenced ServiceAccountToken secret we can mount + serviceAccountToken, err := s.getReferencedServiceAccountToken(serviceAccount) + if err != nil { + fmt.Errorf("Error looking up service account token for %s/%s: %v", serviceAccount.Namespace, serviceAccount.Name, err) + } + if len(serviceAccountToken) == 0 { + // We don't have an API token to mount, so return + return nil + } + + // Find the volume and volume name for the ServiceAccountTokenSecret if it already exists + tokenVolumeName := "" + hasTokenVolume := false + allVolumeNames := util.NewStringSet() + for _, volume := range pod.Spec.Volumes { + allVolumeNames.Insert(volume.Name) + if volume.Secret != nil && volume.Secret.SecretName == serviceAccountToken { + tokenVolumeName = volume.Name + hasTokenVolume = true + break + } + } + + // Determine a volume name for the ServiceAccountTokenSecret in case we need it + if len(tokenVolumeName) == 0 { + // Try naming the volume the same as the serviceAccountToken, and uniquify if needed + tokenVolumeName = serviceAccountToken + if allVolumeNames.Has(tokenVolumeName) { + tokenVolumeName = api.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", serviceAccountToken)) + } + } + + // Create the prototypical VolumeMount + volumeMount := api.VolumeMount{ + Name: tokenVolumeName, + ReadOnly: true, + MountPath: DefaultAPITokenMountPath, + } + + // Ensure every container mounts the APISecret volume + needsTokenVolume := false + for i, container := range pod.Spec.Containers { + existingContainerMount := false + for _, volumeMount := range container.VolumeMounts { + // Existing mounts at the default mount path prevent mounting of the API token + if volumeMount.MountPath == DefaultAPITokenMountPath { + existingContainerMount = true + break + } + } + if !existingContainerMount { + pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, volumeMount) + needsTokenVolume = true + } + } + + // Add the volume if a container needs it + if !hasTokenVolume && needsTokenVolume { + volume := api.Volume{ + Name: tokenVolumeName, + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: serviceAccountToken, + }, + }, + } + pod.Spec.Volumes = append(pod.Spec.Volumes, volume) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount/admission_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount/admission_test.go new file mode 100644 index 000000000000..61f178d160f0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount/admission_test.go @@ -0,0 +1,399 @@ +/* +Copyright 2014 The Kubernetes Authors 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 serviceaccount + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" + "github.com/GoogleCloudPlatform/kubernetes/pkg/types" +) + +func TestIgnoresNonCreate(t *testing.T) { + pod := &api.Pod{} + for _, op := range []string{"UPDATE", "DELETE", "CUSTOM"} { + attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), op) + err := NewServiceAccount(nil).Admit(attrs) + if err != nil { + t.Errorf("Expected %s operation allowed, got err: %v", op, err) + } + } +} + +func TestIgnoresNonPodResource(t *testing.T) { + pod := &api.Pod{} + attrs := admission.NewAttributesRecord(pod, "Pod", "myns", "CustomResource", "CREATE") + err := NewServiceAccount(nil).Admit(attrs) + if err != nil { + t.Errorf("Expected non-pod resource allowed, got err: %v", err) + } +} + +func TestIgnoresNilObject(t *testing.T) { + attrs := admission.NewAttributesRecord(nil, "Pod", "myns", string(api.ResourcePods), "CREATE") + err := NewServiceAccount(nil).Admit(attrs) + if err != nil { + t.Errorf("Expected nil object allowed allowed, got err: %v", err) + } +} + +func TestIgnoresNonPodObject(t *testing.T) { + obj := &api.Namespace{} + attrs := admission.NewAttributesRecord(obj, "Pod", "myns", string(api.ResourcePods), "CREATE") + err := NewServiceAccount(nil).Admit(attrs) + if err != nil { + t.Errorf("Expected non pod object allowed, got err: %v", err) + } +} + +func TestIgnoresMirrorPod(t *testing.T) { + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Annotations: map[string]string{ + kubelet.ConfigMirrorAnnotationKey: "true", + }, + }, + Spec: api.PodSpec{ + Volumes: []api.Volume{ + {VolumeSource: api.VolumeSource{}}, + }, + }, + } + attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), "CREATE") + err := NewServiceAccount(nil).Admit(attrs) + if err != nil { + t.Errorf("Expected mirror pod without service account or secrets allowed, got err: %v", err) + } +} + +func TestRejectsMirrorPodWithServiceAccount(t *testing.T) { + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Annotations: map[string]string{ + kubelet.ConfigMirrorAnnotationKey: "true", + }, + }, + Spec: api.PodSpec{ + ServiceAccount: "default", + }, + } + attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), "CREATE") + err := NewServiceAccount(nil).Admit(attrs) + if err == nil { + t.Errorf("Expected a mirror pod to be prevented from referencing a service account") + } +} + +func TestRejectsMirrorPodWithSecretVolumes(t *testing.T) { + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Annotations: map[string]string{ + kubelet.ConfigMirrorAnnotationKey: "true", + }, + }, + Spec: api.PodSpec{ + Volumes: []api.Volume{ + {VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{}}}, + }, + }, + } + attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), "CREATE") + err := NewServiceAccount(nil).Admit(attrs) + if err == nil { + t.Errorf("Expected a mirror pod to be prevented from referencing a secret volume") + } +} + +func TestAssignsDefaultServiceAccountAndToleratesMissingAPIToken(t *testing.T) { + ns := "myns" + + admit := NewServiceAccount(nil) + admit.MountServiceAccountToken = true + + // Add the default service account for the ns into the cache + admit.serviceAccounts.Add(&api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Name: DefaultServiceAccountName, + Namespace: ns, + }, + }) + + pod := &api.Pod{} + attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE") + err := admit.Admit(attrs) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if pod.Spec.ServiceAccount != DefaultServiceAccountName { + t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount) + } +} + +func TestFetchesUncachedServiceAccount(t *testing.T) { + ns := "myns" + + // Build a test client that the admission plugin can use to look up the service account missing from its cache + client := testclient.NewSimpleFake(&api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Name: DefaultServiceAccountName, + Namespace: ns, + }, + }) + + admit := NewServiceAccount(client) + + pod := &api.Pod{} + attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE") + err := admit.Admit(attrs) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if pod.Spec.ServiceAccount != DefaultServiceAccountName { + t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount) + } +} + +func TestDeniesInvalidServiceAccount(t *testing.T) { + ns := "myns" + + // Build a test client that the admission plugin can use to look up the service account missing from its cache + client := testclient.NewSimpleFake() + + admit := NewServiceAccount(client) + + pod := &api.Pod{} + attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE") + err := admit.Admit(attrs) + if err == nil { + t.Errorf("Expected error for missing service account, got none") + } +} + +func TestAutomountsAPIToken(t *testing.T) { + ns := "myns" + tokenName := "token-name" + serviceAccountName := DefaultServiceAccountName + serviceAccountUID := "12345" + + expectedVolume := api.Volume{ + Name: tokenName, + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{SecretName: tokenName}, + }, + } + expectedVolumeMount := api.VolumeMount{ + Name: tokenName, + ReadOnly: true, + MountPath: DefaultAPITokenMountPath, + } + + admit := NewServiceAccount(nil) + admit.MountServiceAccountToken = true + + // Add the default service account for the ns with a token into the cache + admit.serviceAccounts.Add(&api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Name: serviceAccountName, + Namespace: ns, + UID: types.UID(serviceAccountUID), + }, + Secrets: []api.ObjectReference{ + {Name: tokenName}, + }, + }) + // Add a token for the service account into the cache + admit.secrets.Add(&api.Secret{ + ObjectMeta: api.ObjectMeta{ + Name: tokenName, + Namespace: ns, + Annotations: map[string]string{ + api.ServiceAccountNameKey: serviceAccountName, + api.ServiceAccountUIDKey: serviceAccountUID, + }, + }, + Type: api.SecretTypeServiceAccountToken, + Data: map[string][]byte{ + api.ServiceAccountTokenKey: []byte("token-data"), + }, + }) + + pod := &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{ + {}, + }, + }, + } + attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE") + err := admit.Admit(attrs) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if pod.Spec.ServiceAccount != DefaultServiceAccountName { + t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount) + } + if len(pod.Spec.Volumes) != 1 { + t.Fatalf("Expected 1 volume, got %d", len(pod.Spec.Volumes)) + } + if !reflect.DeepEqual(expectedVolume, pod.Spec.Volumes[0]) { + t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolume, pod.Spec.Volumes[0]) + } + if len(pod.Spec.Containers[0].VolumeMounts) != 1 { + t.Fatalf("Expected 1 volume mount, got %d", len(pod.Spec.Containers[0].VolumeMounts)) + } + if !reflect.DeepEqual(expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0]) { + t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0]) + } +} + +func TestRespectsExistingMount(t *testing.T) { + ns := "myns" + tokenName := "token-name" + serviceAccountName := DefaultServiceAccountName + serviceAccountUID := "12345" + + expectedVolumeMount := api.VolumeMount{ + Name: "my-custom-mount", + ReadOnly: false, + MountPath: DefaultAPITokenMountPath, + } + + admit := NewServiceAccount(nil) + admit.MountServiceAccountToken = true + + // Add the default service account for the ns with a token into the cache + admit.serviceAccounts.Add(&api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Name: serviceAccountName, + Namespace: ns, + UID: types.UID(serviceAccountUID), + }, + Secrets: []api.ObjectReference{ + {Name: tokenName}, + }, + }) + // Add a token for the service account into the cache + admit.secrets.Add(&api.Secret{ + ObjectMeta: api.ObjectMeta{ + Name: tokenName, + Namespace: ns, + Annotations: map[string]string{ + api.ServiceAccountNameKey: serviceAccountName, + api.ServiceAccountUIDKey: serviceAccountUID, + }, + }, + Type: api.SecretTypeServiceAccountToken, + Data: map[string][]byte{ + api.ServiceAccountTokenKey: []byte("token-data"), + }, + }) + + // Define a pod with a container that already mounts a volume at the API token path + // Admission should respect that + // Additionally, no volume should be created if no container is going to use it + pod := &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{ + { + VolumeMounts: []api.VolumeMount{ + expectedVolumeMount, + }, + }, + }, + }, + } + attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE") + err := admit.Admit(attrs) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if pod.Spec.ServiceAccount != DefaultServiceAccountName { + t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount) + } + if len(pod.Spec.Volumes) != 0 { + t.Fatalf("Expected 0 volumes (shouldn't create a volume for a secret we don't need), got %d", len(pod.Spec.Volumes)) + } + if len(pod.Spec.Containers[0].VolumeMounts) != 1 { + t.Fatalf("Expected 1 volume mount, got %d", len(pod.Spec.Containers[0].VolumeMounts)) + } + if !reflect.DeepEqual(expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0]) { + t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0]) + } +} + +func TestAllowsReferencedSecretVolumes(t *testing.T) { + ns := "myns" + + admit := NewServiceAccount(nil) + admit.LimitSecretReferences = true + + // Add the default service account for the ns with a secret reference into the cache + admit.serviceAccounts.Add(&api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Name: DefaultServiceAccountName, + Namespace: ns, + }, + Secrets: []api.ObjectReference{ + {Name: "foo"}, + }, + }) + + pod := &api.Pod{ + Spec: api.PodSpec{ + Volumes: []api.Volume{ + {VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "foo"}}}, + }, + }, + } + attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE") + err := admit.Admit(attrs) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } +} + +func TestRejectsUnreferencedSecretVolumes(t *testing.T) { + ns := "myns" + + admit := NewServiceAccount(nil) + admit.LimitSecretReferences = true + + // Add the default service account for the ns into the cache + admit.serviceAccounts.Add(&api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Name: DefaultServiceAccountName, + Namespace: ns, + }, + }) + + pod := &api.Pod{ + Spec: api.PodSpec{ + Volumes: []api.Volume{ + {VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "foo"}}}, + }, + }, + } + attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE") + err := admit.Admit(attrs) + if err == nil { + t.Errorf("Expected rejection for using a secret the service account does not reference") + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount/doc.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount/doc.go new file mode 100644 index 000000000000..50a8b061498f --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 The Kubernetes Authors 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. +*/ + +// serviceaccount enforces all pods having an associated serviceaccount, +// and all containers mounting the API token for that serviceaccount at a known location +package serviceaccount diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/third_party/forked/coreos/go-etcd/etcd/client.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/third_party/forked/coreos/go-etcd/etcd/client.go new file mode 100644 index 000000000000..7826be806639 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/third_party/forked/coreos/go-etcd/etcd/client.go @@ -0,0 +1,32 @@ +package etcd + +import ( + "errors" + "net" + "time" +) + +// dial attempts to open a TCP connection to the provided address, explicitly +// enabling keep-alives with a one-second interval. +func Dial(network, addr string) (net.Conn, error) { + conn, err := net.DialTimeout(network, addr, time.Second) + if err != nil { + return nil, err + } + + tcpConn, ok := conn.(*net.TCPConn) + if !ok { + return nil, errors.New("Failed type-assertion of net.Conn as *net.TCPConn") + } + + // Keep TCP alive to check whether or not the remote machine is down + if err = tcpConn.SetKeepAlive(true); err != nil { + return nil, err + } + + if err = tcpConn.SetKeepAlivePeriod(time.Second); err != nil { + return nil, err + } + + return tcpConn, nil +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt.go new file mode 100644 index 000000000000..c0654f5d851f --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt.go @@ -0,0 +1,74 @@ +package aws + +import ( + "time" +) + +// AttemptStrategy represents a strategy for waiting for an action +// to complete successfully. This is an internal type used by the +// implementation of other goamz packages. +type AttemptStrategy struct { + Total time.Duration // total duration of attempt. + Delay time.Duration // interval between each try in the burst. + Min int // minimum number of retries; overrides Total +} + +type Attempt struct { + strategy AttemptStrategy + last time.Time + end time.Time + force bool + count int +} + +// Start begins a new sequence of attempts for the given strategy. +func (s AttemptStrategy) Start() *Attempt { + now := time.Now() + return &Attempt{ + strategy: s, + last: now, + end: now.Add(s.Total), + force: true, + } +} + +// Next waits until it is time to perform the next attempt or returns +// false if it is time to stop trying. +func (a *Attempt) Next() bool { + now := time.Now() + sleep := a.nextSleep(now) + if !a.force && !now.Add(sleep).Before(a.end) && a.strategy.Min <= a.count { + return false + } + a.force = false + if sleep > 0 && a.count > 0 { + time.Sleep(sleep) + now = time.Now() + } + a.count++ + a.last = now + return true +} + +func (a *Attempt) nextSleep(now time.Time) time.Duration { + sleep := a.strategy.Delay - now.Sub(a.last) + if sleep < 0 { + return 0 + } + return sleep +} + +// HasNext returns whether another attempt will be made if the current +// one fails. If it returns true, the following call to Next is +// guaranteed to return true. +func (a *Attempt) HasNext() bool { + if a.force || a.strategy.Min > a.count { + return true + } + now := time.Now() + if now.Add(a.nextSleep(now)).Before(a.end) { + a.force = true + return true + } + return false +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt_test.go new file mode 100644 index 000000000000..1fda5bf3c51e --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt_test.go @@ -0,0 +1,57 @@ +package aws_test + +import ( + "github.com/mitchellh/goamz/aws" + . "github.com/motain/gocheck" + "time" +) + +func (S) TestAttemptTiming(c *C) { + testAttempt := aws.AttemptStrategy{ + Total: 0.25e9, + Delay: 0.1e9, + } + want := []time.Duration{0, 0.1e9, 0.2e9, 0.2e9} + got := make([]time.Duration, 0, len(want)) // avoid allocation when testing timing + t0 := time.Now() + for a := testAttempt.Start(); a.Next(); { + got = append(got, time.Now().Sub(t0)) + } + got = append(got, time.Now().Sub(t0)) + c.Assert(got, HasLen, len(want)) + const margin = 0.01e9 + for i, got := range want { + lo := want[i] - margin + hi := want[i] + margin + if got < lo || got > hi { + c.Errorf("attempt %d want %g got %g", i, want[i].Seconds(), got.Seconds()) + } + } +} + +func (S) TestAttemptNextHasNext(c *C) { + a := aws.AttemptStrategy{}.Start() + c.Assert(a.Next(), Equals, true) + c.Assert(a.Next(), Equals, false) + + a = aws.AttemptStrategy{}.Start() + c.Assert(a.Next(), Equals, true) + c.Assert(a.HasNext(), Equals, false) + c.Assert(a.Next(), Equals, false) + + a = aws.AttemptStrategy{Total: 2e8}.Start() + c.Assert(a.Next(), Equals, true) + c.Assert(a.HasNext(), Equals, true) + time.Sleep(2e8) + c.Assert(a.HasNext(), Equals, true) + c.Assert(a.Next(), Equals, true) + c.Assert(a.Next(), Equals, false) + + a = aws.AttemptStrategy{Total: 1e8, Min: 2}.Start() + time.Sleep(1e8) + c.Assert(a.Next(), Equals, true) + c.Assert(a.HasNext(), Equals, true) + c.Assert(a.Next(), Equals, true) + c.Assert(a.HasNext(), Equals, false) + c.Assert(a.Next(), Equals, false) +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws.go new file mode 100644 index 000000000000..cfc42c03ab42 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws.go @@ -0,0 +1,445 @@ +// +// goamz - Go packages to interact with the Amazon Web Services. +// +// https://wiki.ubuntu.com/goamz +// +// Copyright (c) 2011 Canonical Ltd. +// +// Written by Gustavo Niemeyer +// +package aws + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + + "github.com/vaughan0/go-ini" +) + +// Region defines the URLs where AWS services may be accessed. +// +// See http://goo.gl/d8BP1 for more details. +type Region struct { + Name string // the canonical name of this region. + EC2Endpoint string + S3Endpoint string + S3BucketEndpoint string // Not needed by AWS S3. Use ${bucket} for bucket name. + S3LocationConstraint bool // true if this region requires a LocationConstraint declaration. + S3LowercaseBucket bool // true if the region requires bucket names to be lower case. + SDBEndpoint string + SNSEndpoint string + SQSEndpoint string + IAMEndpoint string + ELBEndpoint string + AutoScalingEndpoint string + RdsEndpoint string + Route53Endpoint string +} + +var USGovWest = Region{ + "us-gov-west-1", + "https://ec2.us-gov-west-1.amazonaws.com", + "https://s3-fips-us-gov-west-1.amazonaws.com", + "", + true, + true, + "", + "https://sns.us-gov-west-1.amazonaws.com", + "https://sqs.us-gov-west-1.amazonaws.com", + "https://iam.us-gov.amazonaws.com", + "https://elasticloadbalancing.us-gov-west-1.amazonaws.com", + "https://autoscaling.us-gov-west-1.amazonaws.com", + "https://rds.us-gov-west-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var USEast = Region{ + "us-east-1", + "https://ec2.us-east-1.amazonaws.com", + "https://s3.amazonaws.com", + "", + false, + false, + "https://sdb.amazonaws.com", + "https://sns.us-east-1.amazonaws.com", + "https://sqs.us-east-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.us-east-1.amazonaws.com", + "https://autoscaling.us-east-1.amazonaws.com", + "https://rds.us-east-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var USWest = Region{ + "us-west-1", + "https://ec2.us-west-1.amazonaws.com", + "https://s3-us-west-1.amazonaws.com", + "", + true, + true, + "https://sdb.us-west-1.amazonaws.com", + "https://sns.us-west-1.amazonaws.com", + "https://sqs.us-west-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.us-west-1.amazonaws.com", + "https://autoscaling.us-west-1.amazonaws.com", + "https://rds.us-west-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var USWest2 = Region{ + "us-west-2", + "https://ec2.us-west-2.amazonaws.com", + "https://s3-us-west-2.amazonaws.com", + "", + true, + true, + "https://sdb.us-west-2.amazonaws.com", + "https://sns.us-west-2.amazonaws.com", + "https://sqs.us-west-2.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.us-west-2.amazonaws.com", + "https://autoscaling.us-west-2.amazonaws.com", + "https://rds.us-west-2.amazonaws.com", + "https://route53.amazonaws.com", +} + +var EUWest = Region{ + "eu-west-1", + "https://ec2.eu-west-1.amazonaws.com", + "https://s3-eu-west-1.amazonaws.com", + "", + true, + true, + "https://sdb.eu-west-1.amazonaws.com", + "https://sns.eu-west-1.amazonaws.com", + "https://sqs.eu-west-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.eu-west-1.amazonaws.com", + "https://autoscaling.eu-west-1.amazonaws.com", + "https://rds.eu-west-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var EUCentral = Region{ + "eu-central-1", + "https://ec2.eu-central-1.amazonaws.com", + "https://s3-eu-central-1.amazonaws.com", + "", + true, + true, + "", + "https://sns.eu-central-1.amazonaws.com", + "https://sqs.eu-central-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.eu-central-1.amazonaws.com", + "https://autoscaling.eu-central-1.amazonaws.com", + "https://rds.eu-central-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var APSoutheast = Region{ + "ap-southeast-1", + "https://ec2.ap-southeast-1.amazonaws.com", + "https://s3-ap-southeast-1.amazonaws.com", + "", + true, + true, + "https://sdb.ap-southeast-1.amazonaws.com", + "https://sns.ap-southeast-1.amazonaws.com", + "https://sqs.ap-southeast-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.ap-southeast-1.amazonaws.com", + "https://autoscaling.ap-southeast-1.amazonaws.com", + "https://rds.ap-southeast-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var APSoutheast2 = Region{ + "ap-southeast-2", + "https://ec2.ap-southeast-2.amazonaws.com", + "https://s3-ap-southeast-2.amazonaws.com", + "", + true, + true, + "https://sdb.ap-southeast-2.amazonaws.com", + "https://sns.ap-southeast-2.amazonaws.com", + "https://sqs.ap-southeast-2.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.ap-southeast-2.amazonaws.com", + "https://autoscaling.ap-southeast-2.amazonaws.com", + "https://rds.ap-southeast-2.amazonaws.com", + "https://route53.amazonaws.com", +} + +var APNortheast = Region{ + "ap-northeast-1", + "https://ec2.ap-northeast-1.amazonaws.com", + "https://s3-ap-northeast-1.amazonaws.com", + "", + true, + true, + "https://sdb.ap-northeast-1.amazonaws.com", + "https://sns.ap-northeast-1.amazonaws.com", + "https://sqs.ap-northeast-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.ap-northeast-1.amazonaws.com", + "https://autoscaling.ap-northeast-1.amazonaws.com", + "https://rds.ap-northeast-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var SAEast = Region{ + "sa-east-1", + "https://ec2.sa-east-1.amazonaws.com", + "https://s3-sa-east-1.amazonaws.com", + "", + true, + true, + "https://sdb.sa-east-1.amazonaws.com", + "https://sns.sa-east-1.amazonaws.com", + "https://sqs.sa-east-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.sa-east-1.amazonaws.com", + "https://autoscaling.sa-east-1.amazonaws.com", + "https://rds.sa-east-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var CNNorth = Region{ + "cn-north-1", + "https://ec2.cn-north-1.amazonaws.com.cn", + "https://s3.cn-north-1.amazonaws.com.cn", + "", + true, + true, + "", + "https://sns.cn-north-1.amazonaws.com.cn", + "https://sqs.cn-north-1.amazonaws.com.cn", + "https://iam.cn-north-1.amazonaws.com.cn", + "https://elasticloadbalancing.cn-north-1.amazonaws.com.cn", + "https://autoscaling.cn-north-1.amazonaws.com.cn", + "https://rds.cn-north-1.amazonaws.com.cn", + "https://route53.amazonaws.com", +} + +var Regions = map[string]Region{ + APNortheast.Name: APNortheast, + APSoutheast.Name: APSoutheast, + APSoutheast2.Name: APSoutheast2, + EUWest.Name: EUWest, + EUCentral.Name: EUCentral, + USEast.Name: USEast, + USWest.Name: USWest, + USWest2.Name: USWest2, + SAEast.Name: SAEast, + USGovWest.Name: USGovWest, + CNNorth.Name: CNNorth, +} + +type Auth struct { + AccessKey, SecretKey, Token string +} + +var unreserved = make([]bool, 128) +var hex = "0123456789ABCDEF" + +func init() { + // RFC3986 + u := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-_.~" + for _, c := range u { + unreserved[c] = true + } +} + +type credentials struct { + Code string + LastUpdated string + Type string + AccessKeyId string + SecretAccessKey string + Token string + Expiration string +} + +// GetMetaData retrieves instance metadata about the current machine. +// +// See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html for more details. +func GetMetaData(path string) (contents []byte, err error) { + url := "http://169.254.169.254/latest/meta-data/" + path + + resp, err := RetryingClient.Get(url) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + err = fmt.Errorf("Code %d returned for url %s", resp.StatusCode, url) + return + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + return []byte(body), err +} + +func getInstanceCredentials() (cred credentials, err error) { + credentialPath := "iam/security-credentials/" + + // Get the instance role + role, err := GetMetaData(credentialPath) + if err != nil { + return + } + + // Get the instance role credentials + credentialJSON, err := GetMetaData(credentialPath + string(role)) + if err != nil { + return + } + + err = json.Unmarshal([]byte(credentialJSON), &cred) + return +} + +// GetAuth creates an Auth based on either passed in credentials, +// environment information or instance based role credentials. +func GetAuth(accessKey string, secretKey string) (auth Auth, err error) { + // First try passed in credentials + if accessKey != "" && secretKey != "" { + return Auth{accessKey, secretKey, ""}, nil + } + + // Next try to get auth from the environment + auth, err = SharedAuth() + if err == nil { + // Found auth, return + return + } + + // Next try to get auth from the environment + auth, err = EnvAuth() + if err == nil { + // Found auth, return + return + } + + // Next try getting auth from the instance role + cred, err := getInstanceCredentials() + if err == nil { + // Found auth, return + auth.AccessKey = cred.AccessKeyId + auth.SecretKey = cred.SecretAccessKey + auth.Token = cred.Token + return + } + err = errors.New("No valid AWS authentication found") + return +} + +// SharedAuth creates an Auth based on shared credentials stored in +// $HOME/.aws/credentials. The AWS_PROFILE environment variables is used to +// select the profile. +func SharedAuth() (auth Auth, err error) { + var profileName = os.Getenv("AWS_PROFILE") + + if profileName == "" { + profileName = "default" + } + + var credentialsFile = os.Getenv("AWS_CREDENTIAL_FILE") + if credentialsFile == "" { + var homeDir = os.Getenv("HOME") + if homeDir == "" { + err = errors.New("Could not get HOME") + return + } + credentialsFile = homeDir + "/.aws/credentials" + } + + file, err := ini.LoadFile(credentialsFile) + if err != nil { + err = errors.New("Couldn't parse AWS credentials file") + return + } + + var profile = file[profileName] + if profile == nil { + err = errors.New("Couldn't find profile in AWS credentials file") + return + } + + auth.AccessKey = profile["aws_access_key_id"] + auth.SecretKey = profile["aws_secret_access_key"] + + if auth.AccessKey == "" { + err = errors.New("AWS_ACCESS_KEY_ID not found in environment in credentials file") + } + if auth.SecretKey == "" { + err = errors.New("AWS_SECRET_ACCESS_KEY not found in credentials file") + } + return +} + +// EnvAuth creates an Auth based on environment information. +// The AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment +// For accounts that require a security token, it is read from AWS_SECURITY_TOKEN +// variables are used. +func EnvAuth() (auth Auth, err error) { + auth.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID") + if auth.AccessKey == "" { + auth.AccessKey = os.Getenv("AWS_ACCESS_KEY") + } + + auth.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY") + if auth.SecretKey == "" { + auth.SecretKey = os.Getenv("AWS_SECRET_KEY") + } + + auth.Token = os.Getenv("AWS_SECURITY_TOKEN") + + if auth.AccessKey == "" { + err = errors.New("AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment") + } + if auth.SecretKey == "" { + err = errors.New("AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment") + } + return +} + +// Encode takes a string and URI-encodes it in a way suitable +// to be used in AWS signatures. +func Encode(s string) string { + encode := false + for i := 0; i != len(s); i++ { + c := s[i] + if c > 127 || !unreserved[c] { + encode = true + break + } + } + if !encode { + return s + } + e := make([]byte, len(s)*3) + ei := 0 + for i := 0; i != len(s); i++ { + c := s[i] + if c > 127 || !unreserved[c] { + e[ei] = '%' + e[ei+1] = hex[c>>4] + e[ei+2] = hex[c&0xF] + ei += 3 + } else { + e[ei] = c + ei += 1 + } + } + return string(e[:ei]) +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws_test.go new file mode 100644 index 000000000000..78cbbaf03c52 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws_test.go @@ -0,0 +1,203 @@ +package aws_test + +import ( + "github.com/mitchellh/goamz/aws" + . "github.com/motain/gocheck" + "io/ioutil" + "os" + "strings" + "testing" +) + +func Test(t *testing.T) { + TestingT(t) +} + +var _ = Suite(&S{}) + +type S struct { + environ []string +} + +func (s *S) SetUpSuite(c *C) { + s.environ = os.Environ() +} + +func (s *S) TearDownTest(c *C) { + os.Clearenv() + for _, kv := range s.environ { + l := strings.SplitN(kv, "=", 2) + os.Setenv(l[0], l[1]) + } +} + +func (s *S) TestSharedAuthNoHome(c *C) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "foo") + _, err := aws.SharedAuth() + c.Assert(err, ErrorMatches, "Could not get HOME") +} + +func (s *S) TestSharedAuthNoCredentialsFile(c *C) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "foo") + os.Setenv("HOME", "/tmp") + _, err := aws.SharedAuth() + c.Assert(err, ErrorMatches, "Couldn't parse AWS credentials file") +} + +func (s *S) TestSharedAuthNoProfileInFile(c *C) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "foo") + + d, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(d) + + err = os.Mkdir(d+"/.aws", 0755) + if err != nil { + panic(err) + } + + ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\n"), 0644) + os.Setenv("HOME", d) + + _, err = aws.SharedAuth() + c.Assert(err, ErrorMatches, "Couldn't find profile in AWS credentials file") +} + +func (s *S) TestSharedAuthNoKeysInProfile(c *C) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "bar") + + d, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(d) + + err = os.Mkdir(d+"/.aws", 0755) + if err != nil { + panic(err) + } + + ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\nawsaccesskeyid = AK.."), 0644) + os.Setenv("HOME", d) + + _, err = aws.SharedAuth() + c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_KEY not found in credentials file") +} + +func (s *S) TestSharedAuthDefaultCredentials(c *C) { + os.Clearenv() + + d, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(d) + + err = os.Mkdir(d+"/.aws", 0755) + if err != nil { + panic(err) + } + + ioutil.WriteFile(d+"/.aws/credentials", []byte("[default]\naws_access_key_id = access\naws_secret_access_key = secret\n"), 0644) + os.Setenv("HOME", d) + + auth, err := aws.SharedAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestSharedAuth(c *C) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "bar") + + d, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(d) + + err = os.Mkdir(d+"/.aws", 0755) + if err != nil { + panic(err) + } + + ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\naws_access_key_id = access\naws_secret_access_key = secret\n"), 0644) + os.Setenv("HOME", d) + + auth, err := aws.SharedAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestEnvAuthNoSecret(c *C) { + os.Clearenv() + _, err := aws.EnvAuth() + c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment") +} + +func (s *S) TestEnvAuthNoAccess(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_ACCESS_KEY", "foo") + _, err := aws.EnvAuth() + c.Assert(err, ErrorMatches, "AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment") +} + +func (s *S) TestEnvAuth(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_ACCESS_KEY", "secret") + os.Setenv("AWS_ACCESS_KEY_ID", "access") + auth, err := aws.EnvAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestEnvAuthWithToken(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_ACCESS_KEY", "secret") + os.Setenv("AWS_ACCESS_KEY_ID", "access") + os.Setenv("AWS_SECURITY_TOKEN", "token") + auth, err := aws.EnvAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access", Token: "token"}) +} + +func (s *S) TestEnvAuthAlt(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_KEY", "secret") + os.Setenv("AWS_ACCESS_KEY", "access") + auth, err := aws.EnvAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestGetAuthStatic(c *C) { + auth, err := aws.GetAuth("access", "secret") + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestGetAuthEnv(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_ACCESS_KEY", "secret") + os.Setenv("AWS_ACCESS_KEY_ID", "access") + auth, err := aws.GetAuth("", "") + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestEncode(c *C) { + c.Assert(aws.Encode("foo"), Equals, "foo") + c.Assert(aws.Encode("/"), Equals, "%2F") +} + +func (s *S) TestRegionsAreNamed(c *C) { + for n, r := range aws.Regions { + c.Assert(n, Equals, r.Name) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client.go new file mode 100644 index 000000000000..ee53238f7b3c --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client.go @@ -0,0 +1,125 @@ +package aws + +import ( + "math" + "net" + "net/http" + "time" +) + +type RetryableFunc func(*http.Request, *http.Response, error) bool +type WaitFunc func(try int) +type DeadlineFunc func() time.Time + +type ResilientTransport struct { + // Timeout is the maximum amount of time a dial will wait for + // a connect to complete. + // + // The default is no timeout. + // + // With or without a timeout, the operating system may impose + // its own earlier timeout. For instance, TCP timeouts are + // often around 3 minutes. + DialTimeout time.Duration + + // MaxTries, if non-zero, specifies the number of times we will retry on + // failure. Retries are only attempted for temporary network errors or known + // safe failures. + MaxTries int + Deadline DeadlineFunc + ShouldRetry RetryableFunc + Wait WaitFunc + transport *http.Transport +} + +// Convenience method for creating an http client +func NewClient(rt *ResilientTransport) *http.Client { + rt.transport = &http.Transport{ + Dial: func(netw, addr string) (net.Conn, error) { + c, err := net.DialTimeout(netw, addr, rt.DialTimeout) + if err != nil { + return nil, err + } + c.SetDeadline(rt.Deadline()) + return c, nil + }, + DisableKeepAlives: true, + Proxy: http.ProxyFromEnvironment, + } + // TODO: Would be nice is ResilientTransport allowed clients to initialize + // with http.Transport attributes. + return &http.Client{ + Transport: rt, + } +} + +var retryingTransport = &ResilientTransport{ + Deadline: func() time.Time { + return time.Now().Add(5 * time.Second) + }, + DialTimeout: 10 * time.Second, + MaxTries: 3, + ShouldRetry: awsRetry, + Wait: ExpBackoff, +} + +// Exported default client +var RetryingClient = NewClient(retryingTransport) + +func (t *ResilientTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return t.tries(req) +} + +// Retry a request a maximum of t.MaxTries times. +// We'll only retry if the proper criteria are met. +// If a wait function is specified, wait that amount of time +// In between requests. +func (t *ResilientTransport) tries(req *http.Request) (res *http.Response, err error) { + for try := 0; try < t.MaxTries; try += 1 { + res, err = t.transport.RoundTrip(req) + + if !t.ShouldRetry(req, res, err) { + break + } + if res != nil { + res.Body.Close() + } + if t.Wait != nil { + t.Wait(try) + } + } + + return +} + +func ExpBackoff(try int) { + time.Sleep(100 * time.Millisecond * + time.Duration(math.Exp2(float64(try)))) +} + +func LinearBackoff(try int) { + time.Sleep(time.Duration(try*100) * time.Millisecond) +} + +// Decide if we should retry a request. +// In general, the criteria for retrying a request is described here +// http://docs.aws.amazon.com/general/latest/gr/api-retries.html +func awsRetry(req *http.Request, res *http.Response, err error) bool { + retry := false + + // Retry if there's a temporary network error. + if neterr, ok := err.(net.Error); ok { + if neterr.Temporary() { + retry = true + } + } + + // Retry if we get a 5xx series error. + if res != nil { + if res.StatusCode >= 500 && res.StatusCode < 600 { + retry = true + } + } + + return retry +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client_test.go new file mode 100644 index 000000000000..2f6b39cf3aff --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client_test.go @@ -0,0 +1,121 @@ +package aws_test + +import ( + "fmt" + "github.com/mitchellh/goamz/aws" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// Retrieve the response from handler using aws.RetryingClient +func serveAndGet(handler http.HandlerFunc) (body string, err error) { + ts := httptest.NewServer(handler) + defer ts.Close() + resp, err := aws.RetryingClient.Get(ts.URL) + if err != nil { + return + } + if resp.StatusCode != 200 { + return "", fmt.Errorf("Bad status code: %d", resp.StatusCode) + } + greeting, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return + } + return strings.TrimSpace(string(greeting)), nil +} + +func TestClient_expected(t *testing.T) { + body := "foo bar" + + resp, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, body) + }) + if err != nil { + t.Fatal(err) + } + if resp != body { + t.Fatal("Body not as expected.") + } +} + +func TestClient_delay(t *testing.T) { + body := "baz" + wait := 4 + resp, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) { + if wait < 0 { + // If we dipped to zero delay and still failed. + t.Fatal("Never succeeded.") + } + wait -= 1 + time.Sleep(time.Second * time.Duration(wait)) + fmt.Fprintln(w, body) + }) + if err != nil { + t.Fatal(err) + } + if resp != body { + t.Fatal("Body not as expected.", resp) + } +} + +func TestClient_no4xxRetry(t *testing.T) { + tries := 0 + + // Fail once before succeeding. + _, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) { + tries += 1 + http.Error(w, "error", 404) + }) + + if err == nil { + t.Fatal("should have error") + } + + if tries != 1 { + t.Fatalf("should only try once: %d", tries) + } +} + +func TestClient_retries(t *testing.T) { + body := "biz" + failed := false + // Fail once before succeeding. + resp, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) { + if !failed { + http.Error(w, "error", 500) + failed = true + } else { + fmt.Fprintln(w, body) + } + }) + if failed != true { + t.Error("We didn't retry!") + } + if err != nil { + t.Fatal(err) + } + if resp != body { + t.Fatal("Body not as expected.") + } +} + +func TestClient_fails(t *testing.T) { + tries := 0 + // Fail 3 times and return the last error. + _, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) { + tries += 1 + http.Error(w, "error", 500) + }) + if err == nil { + t.Fatal(err) + } + if tries != 3 { + t.Fatal("Didn't retry enough") + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2.go new file mode 100644 index 000000000000..c0c9b6712bf5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2.go @@ -0,0 +1,3087 @@ +// +// goamz - Go packages to interact with the Amazon Web Services. +// +// https://wiki.ubuntu.com/goamz +// +// Copyright (c) 2011 Canonical Ltd. +// +// Written by Gustavo Niemeyer +// + +package ec2 + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/xml" + "fmt" + "log" + "net/http" + "net/http/httputil" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "github.com/mitchellh/goamz/aws" +) + +const debug = false + +// The EC2 type encapsulates operations with a specific EC2 region. +type EC2 struct { + aws.Auth + aws.Region + httpClient *http.Client + private byte // Reserve the right of using private data. +} + +// New creates a new EC2. +func NewWithClient(auth aws.Auth, region aws.Region, client *http.Client) *EC2 { + return &EC2{auth, region, client, 0} +} + +func New(auth aws.Auth, region aws.Region) *EC2 { + return NewWithClient(auth, region, aws.RetryingClient) +} + +// ---------------------------------------------------------------------------- +// Filtering helper. + +// Filter builds filtering parameters to be used in an EC2 query which supports +// filtering. For example: +// +// filter := NewFilter() +// filter.Add("architecture", "i386") +// filter.Add("launch-index", "0") +// resp, err := ec2.Instances(nil, filter) +// +type Filter struct { + m map[string][]string +} + +// NewFilter creates a new Filter. +func NewFilter() *Filter { + return &Filter{make(map[string][]string)} +} + +// Add appends a filtering parameter with the given name and value(s). +func (f *Filter) Add(name string, value ...string) { + f.m[name] = append(f.m[name], value...) +} + +func (f *Filter) addParams(params map[string]string) { + if f != nil { + a := make([]string, len(f.m)) + i := 0 + for k := range f.m { + a[i] = k + i++ + } + sort.StringSlice(a).Sort() + for i, k := range a { + prefix := "Filter." + strconv.Itoa(i+1) + params[prefix+".Name"] = k + for j, v := range f.m[k] { + params[prefix+".Value."+strconv.Itoa(j+1)] = v + } + } + } +} + +// ---------------------------------------------------------------------------- +// Request dispatching logic. + +// Error encapsulates an error returned by EC2. +// +// See http://goo.gl/VZGuC for more details. +type Error struct { + // HTTP status code (200, 403, ...) + StatusCode int + // EC2 error code ("UnsupportedOperation", ...) + Code string + // The human-oriented error message + Message string + RequestId string `xml:"RequestID"` +} + +func (err *Error) Error() string { + if err.Code == "" { + return err.Message + } + + return fmt.Sprintf("%s (%s)", err.Message, err.Code) +} + +// For now a single error inst is being exposed. In the future it may be useful +// to provide access to all of them, but rather than doing it as an array/slice, +// use a *next pointer, so that it's backward compatible and it continues to be +// easy to handle the first error, which is what most people will want. +type xmlErrors struct { + RequestId string `xml:"RequestID"` + Errors []Error `xml:"Errors>Error"` +} + +var timeNow = time.Now + +func (ec2 *EC2) query(params map[string]string, resp interface{}) error { + params["Version"] = "2014-06-15" + params["Timestamp"] = timeNow().In(time.UTC).Format(time.RFC3339) + endpoint, err := url.Parse(ec2.Region.EC2Endpoint) + if err != nil { + return err + } + if endpoint.Path == "" { + endpoint.Path = "/" + } + sign(ec2.Auth, "GET", endpoint.Path, params, endpoint.Host) + endpoint.RawQuery = multimap(params).Encode() + if debug { + log.Printf("get { %v } -> {\n", endpoint.String()) + } + + r, err := ec2.httpClient.Get(endpoint.String()) + if err != nil { + return err + } + defer r.Body.Close() + + if debug { + dump, _ := httputil.DumpResponse(r, true) + log.Printf("response:\n") + log.Printf("%v\n}\n", string(dump)) + } + if r.StatusCode != 200 { + return buildError(r) + } + err = xml.NewDecoder(r.Body).Decode(resp) + return err +} + +func multimap(p map[string]string) url.Values { + q := make(url.Values, len(p)) + for k, v := range p { + q[k] = []string{v} + } + return q +} + +func buildError(r *http.Response) error { + errors := xmlErrors{} + xml.NewDecoder(r.Body).Decode(&errors) + var err Error + if len(errors.Errors) > 0 { + err = errors.Errors[0] + } + err.RequestId = errors.RequestId + err.StatusCode = r.StatusCode + if err.Message == "" { + err.Message = err.Code + } + return &err +} + +func makeParams(action string) map[string]string { + params := make(map[string]string) + params["Action"] = action + return params +} + +func addParamsList(params map[string]string, label string, ids []string) { + for i, id := range ids { + params[label+"."+strconv.Itoa(i+1)] = id + } +} + +func addBlockDeviceParams(prename string, params map[string]string, blockdevices []BlockDeviceMapping) { + for i, k := range blockdevices { + // Fixup index since Amazon counts these from 1 + prefix := prename + "BlockDeviceMapping." + strconv.Itoa(i+1) + "." + + if k.DeviceName != "" { + params[prefix+"DeviceName"] = k.DeviceName + } + + if k.VirtualName != "" { + params[prefix+"VirtualName"] = k.VirtualName + } else if k.NoDevice { + params[prefix+"NoDevice"] = "" + } else { + if k.SnapshotId != "" { + params[prefix+"Ebs.SnapshotId"] = k.SnapshotId + } + if k.VolumeType != "" { + params[prefix+"Ebs.VolumeType"] = k.VolumeType + } + if k.IOPS != 0 { + params[prefix+"Ebs.Iops"] = strconv.FormatInt(k.IOPS, 10) + } + if k.VolumeSize != 0 { + params[prefix+"Ebs.VolumeSize"] = strconv.FormatInt(k.VolumeSize, 10) + } + if k.DeleteOnTermination { + params[prefix+"Ebs.DeleteOnTermination"] = "true" + } else { + params[prefix+"Ebs.DeleteOnTermination"] = "false" + } + if k.Encrypted { + params[prefix+"Ebs.Encrypted"] = "true" + } + } + } +} + +// ---------------------------------------------------------------------------- +// Instance management functions and types. + +// The RunInstances type encapsulates options for the respective request in EC2. +// +// See http://goo.gl/Mcm3b for more details. +type RunInstances struct { + ImageId string + MinCount int + MaxCount int + KeyName string + InstanceType string + SecurityGroups []SecurityGroup + IamInstanceProfile string + KernelId string + RamdiskId string + UserData []byte + AvailZone string + PlacementGroupName string + Monitoring bool + SubnetId string + AssociatePublicIpAddress bool + DisableAPITermination bool + EbsOptimized bool + ShutdownBehavior string + PrivateIPAddress string + BlockDevices []BlockDeviceMapping + Tenancy string +} + +// Response to a RunInstances request. +// +// See http://goo.gl/Mcm3b for more details. +type RunInstancesResp struct { + RequestId string `xml:"requestId"` + ReservationId string `xml:"reservationId"` + OwnerId string `xml:"ownerId"` + SecurityGroups []SecurityGroup `xml:"groupSet>item"` + Instances []Instance `xml:"instancesSet>item"` +} + +// BlockDevice represents the association of a block device with an instance. +type BlockDevice struct { + DeviceName string `xml:"deviceName"` + VolumeId string `xml:"ebs>volumeId"` + Status string `xml:"ebs>status"` + AttachTime string `xml:"ebs>attachTime"` + DeleteOnTermination bool `xml:"ebs>deleteOnTermination"` +} + +// Instance encapsulates a running instance in EC2. +// +// See http://goo.gl/OCH8a for more details. +type Instance struct { + InstanceId string `xml:"instanceId"` + InstanceType string `xml:"instanceType"` + ImageId string `xml:"imageId"` + PrivateDNSName string `xml:"privateDnsName"` + DNSName string `xml:"dnsName"` + KeyName string `xml:"keyName"` + AMILaunchIndex int `xml:"amiLaunchIndex"` + Hypervisor string `xml:"hypervisor"` + VirtType string `xml:"virtualizationType"` + Monitoring string `xml:"monitoring>state"` + AvailZone string `xml:"placement>availabilityZone"` + Tenancy string `xml:"placement>tenancy"` + PlacementGroupName string `xml:"placement>groupName"` + State InstanceState `xml:"instanceState"` + Tags []Tag `xml:"tagSet>item"` + VpcId string `xml:"vpcId"` + SubnetId string `xml:"subnetId"` + IamInstanceProfile string `xml:"iamInstanceProfile"` + PrivateIpAddress string `xml:"privateIpAddress"` + PublicIpAddress string `xml:"ipAddress"` + Architecture string `xml:"architecture"` + LaunchTime time.Time `xml:"launchTime"` + SourceDestCheck bool `xml:"sourceDestCheck"` + SecurityGroups []SecurityGroup `xml:"groupSet>item"` + EbsOptimized string `xml:"ebsOptimized"` + BlockDevices []BlockDevice `xml:"blockDeviceMapping>item"` +} + +// RunInstances starts new instances in EC2. +// If options.MinCount and options.MaxCount are both zero, a single instance +// will be started; otherwise if options.MaxCount is zero, options.MinCount +// will be used insteead. +// +// See http://goo.gl/Mcm3b for more details. +func (ec2 *EC2) RunInstances(options *RunInstances) (resp *RunInstancesResp, err error) { + params := makeParams("RunInstances") + params["ImageId"] = options.ImageId + params["InstanceType"] = options.InstanceType + var min, max int + if options.MinCount == 0 && options.MaxCount == 0 { + min = 1 + max = 1 + } else if options.MaxCount == 0 { + min = options.MinCount + max = min + } else { + min = options.MinCount + max = options.MaxCount + } + params["MinCount"] = strconv.Itoa(min) + params["MaxCount"] = strconv.Itoa(max) + token, err := clientToken() + if err != nil { + return nil, err + } + params["ClientToken"] = token + + if options.KeyName != "" { + params["KeyName"] = options.KeyName + } + if options.KernelId != "" { + params["KernelId"] = options.KernelId + } + if options.RamdiskId != "" { + params["RamdiskId"] = options.RamdiskId + } + if options.UserData != nil { + userData := make([]byte, b64.EncodedLen(len(options.UserData))) + b64.Encode(userData, options.UserData) + params["UserData"] = string(userData) + } + if options.AvailZone != "" { + params["Placement.AvailabilityZone"] = options.AvailZone + } + if options.PlacementGroupName != "" { + params["Placement.GroupName"] = options.PlacementGroupName + } + if options.Monitoring { + params["Monitoring.Enabled"] = "true" + } + if options.Tenancy != "" { + params["Placement.Tenancy"] = options.Tenancy + } + if options.SubnetId != "" && options.AssociatePublicIpAddress { + // If we have a non-default VPC / Subnet specified, we can flag + // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. + // You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise + // you get: Network interfaces and an instance-level subnet ID may not be specified on the same request + // You also need to attach Security Groups to the NetworkInterface instead of the instance, + // to avoid: Network interfaces and an instance-level security groups may not be specified on + // the same request + params["NetworkInterface.0.DeviceIndex"] = "0" + params["NetworkInterface.0.AssociatePublicIpAddress"] = "true" + params["NetworkInterface.0.SubnetId"] = options.SubnetId + + if options.PrivateIPAddress != "" { + params["NetworkInterface.0.PrivateIpAddress"] = options.PrivateIPAddress + } + + i := 1 + for _, g := range options.SecurityGroups { + // We only have SecurityGroupId's on NetworkInterface's, no SecurityGroup params. + if g.Id != "" { + params["NetworkInterface.0.SecurityGroupId."+strconv.Itoa(i)] = g.Id + i++ + } + } + } else { + if options.SubnetId != "" { + params["SubnetId"] = options.SubnetId + } + + if options.PrivateIPAddress != "" { + params["PrivateIpAddress"] = options.PrivateIPAddress + } + + i, j := 1, 1 + for _, g := range options.SecurityGroups { + if g.Id != "" { + params["SecurityGroupId."+strconv.Itoa(i)] = g.Id + i++ + } else { + params["SecurityGroup."+strconv.Itoa(j)] = g.Name + j++ + } + } + } + if options.IamInstanceProfile != "" { + params["IamInstanceProfile.Name"] = options.IamInstanceProfile + } + if options.DisableAPITermination { + params["DisableApiTermination"] = "true" + } + if options.EbsOptimized { + params["EbsOptimized"] = "true" + } + if options.ShutdownBehavior != "" { + params["InstanceInitiatedShutdownBehavior"] = options.ShutdownBehavior + } + addBlockDeviceParams("", params, options.BlockDevices) + + resp = &RunInstancesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +func clientToken() (string, error) { + // Maximum EC2 client token size is 64 bytes. + // Each byte expands to two when hex encoded. + buf := make([]byte, 32) + _, err := rand.Read(buf) + if err != nil { + return "", err + } + return hex.EncodeToString(buf), nil +} + +// The GetConsoleOutput type encapsulates options for the respective request in EC2. +// +// See http://goo.gl/EY70zb for more details. +type GetConsoleOutput struct { + InstanceId string +} + +// Response to a GetConsoleOutput request. Note that Output is base64-encoded, +// as in the underlying AWS API. +// +// See http://goo.gl/EY70zb for more details. +type GetConsoleOutputResp struct { + RequestId string `xml:"requestId"` + InstanceId string `xml:"instanceId"` + Timestamp time.Time `xml:"timestamp"` + Output string `xml:"output"` +} + +// GetConsoleOutput returns the console output for the sepcified instance. Note +// that console output is base64-encoded, as in the underlying AWS API. +// +// See http://goo.gl/EY70zb for more details. +func (ec2 *EC2) GetConsoleOutput(options *GetConsoleOutput) (resp *GetConsoleOutputResp, err error) { + params := makeParams("GetConsoleOutput") + params["InstanceId"] = options.InstanceId + resp = &GetConsoleOutputResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ---------------------------------------------------------------------------- +// Instance events and status functions and types. + +// The DescribeInstanceStatus type encapsulates options for the respective request in EC2. +// +// See http://goo.gl/DFySJY for more details. +type EventsSet struct { + Code string `xml:"code"` + Description string `xml:"description"` + NotBefore string `xml:"notBefore"` + NotAfter string `xml:"notAfter"` +} + +type StatusDetails struct { + Name string `xml:"name"` + Status string `xml:"status"` + ImpairedSince string `xml:"impairedSince"` +} + +type Status struct { + Status string `xml:"status"` + Details []StatusDetails `xml:"details>item"` +} + +type InstanceStatusSet struct { + InstanceId string `xml:"instanceId"` + AvailabilityZone string `xml:"availabilityZone"` + InstanceState InstanceState `xml:"instanceState"` + SystemStatus Status `xml:"systemStatus"` + InstanceStatus Status `xml:"instanceStatus"` + Events []EventsSet `xml:"eventsSet>item"` +} + +type DescribeInstanceStatusResp struct { + RequestId string `xml:"requestId"` + InstanceStatus []InstanceStatusSet `xml:"instanceStatusSet>item"` +} + +type DescribeInstanceStatus struct { + InstanceIds []string + IncludeAllInstances bool + MaxResults int64 + NextToken string +} + +func (ec2 *EC2) DescribeInstanceStatus(options *DescribeInstanceStatus, filter *Filter) (resp *DescribeInstanceStatusResp, err error) { + params := makeParams("DescribeInstanceStatus") + if options.IncludeAllInstances { + params["IncludeAllInstances"] = "true" + } + if len(options.InstanceIds) > 0 { + addParamsList(params, "InstanceId", options.InstanceIds) + } + if options.MaxResults > 0 { + params["MaxResults"] = strconv.FormatInt(options.MaxResults, 10) + } + if options.NextToken != "" { + params["NextToken"] = options.NextToken + } + if filter != nil { + filter.addParams(params) + } + + resp = &DescribeInstanceStatusResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// ---------------------------------------------------------------------------- +// Spot Instance management functions and types. + +// The RequestSpotInstances type encapsulates options for the respective request in EC2. +// +// See http://goo.gl/GRZgCD for more details. +type RequestSpotInstances struct { + SpotPrice string + InstanceCount int + Type string + ImageId string + KeyName string + InstanceType string + SecurityGroups []SecurityGroup + IamInstanceProfile string + KernelId string + RamdiskId string + UserData []byte + AvailZone string + PlacementGroupName string + Monitoring bool + SubnetId string + AssociatePublicIpAddress bool + PrivateIPAddress string + BlockDevices []BlockDeviceMapping +} + +type SpotInstanceSpec struct { + ImageId string + KeyName string + InstanceType string + SecurityGroups []SecurityGroup + IamInstanceProfile string + KernelId string + RamdiskId string + UserData []byte + AvailZone string + PlacementGroupName string + Monitoring bool + SubnetId string + AssociatePublicIpAddress bool + PrivateIPAddress string + BlockDevices []BlockDeviceMapping +} + +type SpotLaunchSpec struct { + ImageId string `xml:"imageId"` + KeyName string `xml:"keyName"` + InstanceType string `xml:"instanceType"` + SecurityGroups []SecurityGroup `xml:"groupSet>item"` + IamInstanceProfile string `xml:"iamInstanceProfile"` + KernelId string `xml:"kernelId"` + RamdiskId string `xml:"ramdiskId"` + PlacementGroupName string `xml:"placement>groupName"` + Monitoring bool `xml:"monitoring>enabled"` + SubnetId string `xml:"subnetId"` + BlockDevices []BlockDeviceMapping `xml:"blockDeviceMapping>item"` +} + +type SpotStatus struct { + Code string `xml:"code"` + UpdateTime string `xml:"updateTime"` + Message string `xml:"message"` +} + +type SpotRequestResult struct { + SpotRequestId string `xml:"spotInstanceRequestId"` + SpotPrice string `xml:"spotPrice"` + Type string `xml:"type"` + AvailZone string `xml:"launchedAvailabilityZone"` + InstanceId string `xml:"instanceId"` + State string `xml:"state"` + Status SpotStatus `xml:"status"` + SpotLaunchSpec SpotLaunchSpec `xml:"launchSpecification"` + CreateTime string `xml:"createTime"` + Tags []Tag `xml:"tagSet>item"` +} + +// Response to a RequestSpotInstances request. +// +// See http://goo.gl/GRZgCD for more details. +type RequestSpotInstancesResp struct { + RequestId string `xml:"requestId"` + SpotRequestResults []SpotRequestResult `xml:"spotInstanceRequestSet>item"` +} + +// RequestSpotInstances requests a new spot instances in EC2. +func (ec2 *EC2) RequestSpotInstances(options *RequestSpotInstances) (resp *RequestSpotInstancesResp, err error) { + params := makeParams("RequestSpotInstances") + prefix := "LaunchSpecification" + "." + + params["SpotPrice"] = options.SpotPrice + params[prefix+"ImageId"] = options.ImageId + params[prefix+"InstanceType"] = options.InstanceType + + if options.InstanceCount != 0 { + params["InstanceCount"] = strconv.Itoa(options.InstanceCount) + } + if options.KeyName != "" { + params[prefix+"KeyName"] = options.KeyName + } + if options.KernelId != "" { + params[prefix+"KernelId"] = options.KernelId + } + if options.RamdiskId != "" { + params[prefix+"RamdiskId"] = options.RamdiskId + } + if options.UserData != nil { + userData := make([]byte, b64.EncodedLen(len(options.UserData))) + b64.Encode(userData, options.UserData) + params[prefix+"UserData"] = string(userData) + } + if options.AvailZone != "" { + params[prefix+"Placement.AvailabilityZone"] = options.AvailZone + } + if options.PlacementGroupName != "" { + params[prefix+"Placement.GroupName"] = options.PlacementGroupName + } + if options.Monitoring { + params[prefix+"Monitoring.Enabled"] = "true" + } + if options.SubnetId != "" && options.AssociatePublicIpAddress { + // If we have a non-default VPC / Subnet specified, we can flag + // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. + // You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise + // you get: Network interfaces and an instance-level subnet ID may not be specified on the same request + // You also need to attach Security Groups to the NetworkInterface instead of the instance, + // to avoid: Network interfaces and an instance-level security groups may not be specified on + // the same request + params[prefix+"NetworkInterface.0.DeviceIndex"] = "0" + params[prefix+"NetworkInterface.0.AssociatePublicIpAddress"] = "true" + params[prefix+"NetworkInterface.0.SubnetId"] = options.SubnetId + + i := 1 + for _, g := range options.SecurityGroups { + // We only have SecurityGroupId's on NetworkInterface's, no SecurityGroup params. + if g.Id != "" { + params[prefix+"NetworkInterface.0.SecurityGroupId."+strconv.Itoa(i)] = g.Id + i++ + } + } + } else { + if options.SubnetId != "" { + params[prefix+"SubnetId"] = options.SubnetId + } + + i, j := 1, 1 + for _, g := range options.SecurityGroups { + if g.Id != "" { + params[prefix+"SecurityGroupId."+strconv.Itoa(i)] = g.Id + i++ + } else { + params[prefix+"SecurityGroup."+strconv.Itoa(j)] = g.Name + j++ + } + } + } + if options.IamInstanceProfile != "" { + params[prefix+"IamInstanceProfile.Name"] = options.IamInstanceProfile + } + if options.PrivateIPAddress != "" { + params[prefix+"PrivateIpAddress"] = options.PrivateIPAddress + } + addBlockDeviceParams(prefix, params, options.BlockDevices) + + resp = &RequestSpotInstancesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Response to a DescribeSpotInstanceRequests request. +// +// See http://goo.gl/KsKJJk for more details. +type SpotRequestsResp struct { + RequestId string `xml:"requestId"` + SpotRequestResults []SpotRequestResult `xml:"spotInstanceRequestSet>item"` +} + +// DescribeSpotInstanceRequests returns details about spot requests in EC2. Both parameters +// are optional, and if provided will limit the spot requests returned to those +// matching the given spot request ids or filtering rules. +// +// See http://goo.gl/KsKJJk for more details. +func (ec2 *EC2) DescribeSpotRequests(spotrequestIds []string, filter *Filter) (resp *SpotRequestsResp, err error) { + params := makeParams("DescribeSpotInstanceRequests") + addParamsList(params, "SpotInstanceRequestId", spotrequestIds) + filter.addParams(params) + resp = &SpotRequestsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Response to a CancelSpotInstanceRequests request. +// +// See http://goo.gl/3BKHj for more details. +type CancelSpotRequestResult struct { + SpotRequestId string `xml:"spotInstanceRequestId"` + State string `xml:"state"` +} +type CancelSpotRequestsResp struct { + RequestId string `xml:"requestId"` + CancelSpotRequestResults []CancelSpotRequestResult `xml:"spotInstanceRequestSet>item"` +} + +// CancelSpotRequests requests the cancellation of spot requests when the given ids. +// +// See http://goo.gl/3BKHj for more details. +func (ec2 *EC2) CancelSpotRequests(spotrequestIds []string) (resp *CancelSpotRequestsResp, err error) { + params := makeParams("CancelSpotInstanceRequests") + addParamsList(params, "SpotInstanceRequestId", spotrequestIds) + resp = &CancelSpotRequestsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +type DescribeSpotPriceHistory struct { + InstanceType []string + ProductDescription []string + AvailabilityZone string + StartTime, EndTime time.Time +} + +// Response to a DescribeSpotPriceHisotyr request. +// +// See http://goo.gl/3BKHj for more details. +type DescribeSpotPriceHistoryResp struct { + RequestId string `xml:"requestId"` + History []SpotPriceHistory `xml:"spotPriceHistorySet>item"` +} + +type SpotPriceHistory struct { + InstanceType string `xml:"instanceType"` + ProductDescription string `xml:"productDescription"` + SpotPrice string `xml:"spotPrice"` + Timestamp time.Time `xml:"timestamp"` + AvailabilityZone string `xml:"availabilityZone"` +} + +// DescribeSpotPriceHistory gets the spot pricing history. +// +// See http://goo.gl/3BKHj for more details. +func (ec2 *EC2) DescribeSpotPriceHistory(o *DescribeSpotPriceHistory) (resp *DescribeSpotPriceHistoryResp, err error) { + params := makeParams("DescribeSpotPriceHistory") + if o.AvailabilityZone != "" { + params["AvailabilityZone"] = o.AvailabilityZone + } + + if !o.StartTime.IsZero() { + params["StartTime"] = o.StartTime.In(time.UTC).Format(time.RFC3339) + } + if !o.EndTime.IsZero() { + params["EndTime"] = o.EndTime.In(time.UTC).Format(time.RFC3339) + } + + if len(o.InstanceType) > 0 { + addParamsList(params, "InstanceType", o.InstanceType) + } + if len(o.ProductDescription) > 0 { + addParamsList(params, "ProductDescription", o.ProductDescription) + } + + resp = &DescribeSpotPriceHistoryResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Response to a TerminateInstances request. +// +// See http://goo.gl/3BKHj for more details. +type TerminateInstancesResp struct { + RequestId string `xml:"requestId"` + StateChanges []InstanceStateChange `xml:"instancesSet>item"` +} + +// InstanceState encapsulates the state of an instance in EC2. +// +// See http://goo.gl/y3ZBq for more details. +type InstanceState struct { + Code int `xml:"code"` // Watch out, bits 15-8 have unpublished meaning. + Name string `xml:"name"` +} + +// InstanceStateChange informs of the previous and current states +// for an instance when a state change is requested. +type InstanceStateChange struct { + InstanceId string `xml:"instanceId"` + CurrentState InstanceState `xml:"currentState"` + PreviousState InstanceState `xml:"previousState"` +} + +// TerminateInstances requests the termination of instances when the given ids. +// +// See http://goo.gl/3BKHj for more details. +func (ec2 *EC2) TerminateInstances(instIds []string) (resp *TerminateInstancesResp, err error) { + params := makeParams("TerminateInstances") + addParamsList(params, "InstanceId", instIds) + resp = &TerminateInstancesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Response to a DescribeInstances request. +// +// See http://goo.gl/mLbmw for more details. +type InstancesResp struct { + RequestId string `xml:"requestId"` + Reservations []Reservation `xml:"reservationSet>item"` +} + +// Reservation represents details about a reservation in EC2. +// +// See http://goo.gl/0ItPT for more details. +type Reservation struct { + ReservationId string `xml:"reservationId"` + OwnerId string `xml:"ownerId"` + RequesterId string `xml:"requesterId"` + SecurityGroups []SecurityGroup `xml:"groupSet>item"` + Instances []Instance `xml:"instancesSet>item"` +} + +// Instances returns details about instances in EC2. Both parameters +// are optional, and if provided will limit the instances returned to those +// matching the given instance ids or filtering rules. +// +// See http://goo.gl/4No7c for more details. +func (ec2 *EC2) Instances(instIds []string, filter *Filter) (resp *InstancesResp, err error) { + params := makeParams("DescribeInstances") + addParamsList(params, "InstanceId", instIds) + filter.addParams(params) + resp = &InstancesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ---------------------------------------------------------------------------- +// Volume management + +// The CreateVolume request parameters +// +// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateVolume.html +type CreateVolume struct { + AvailZone string + Size int64 + SnapshotId string + VolumeType string + IOPS int64 + Encrypted bool +} + +// Response to an AttachVolume request +type AttachVolumeResp struct { + RequestId string `xml:"requestId"` + VolumeId string `xml:"volumeId"` + InstanceId string `xml:"instanceId"` + Device string `xml:"device"` + Status string `xml:"status"` + AttachTime string `xml:"attachTime"` +} + +// Response to a CreateVolume request +type CreateVolumeResp struct { + RequestId string `xml:"requestId"` + VolumeId string `xml:"volumeId"` + Size int64 `xml:"size"` + SnapshotId string `xml:"snapshotId"` + AvailZone string `xml:"availabilityZone"` + Status string `xml:"status"` + CreateTime string `xml:"createTime"` + VolumeType string `xml:"volumeType"` + IOPS int64 `xml:"iops"` + Encrypted bool `xml:"encrypted"` +} + +// Volume is a single volume. +type Volume struct { + VolumeId string `xml:"volumeId"` + Size string `xml:"size"` + SnapshotId string `xml:"snapshotId"` + AvailZone string `xml:"availabilityZone"` + Status string `xml:"status"` + Attachments []VolumeAttachment `xml:"attachmentSet>item"` + VolumeType string `xml:"volumeType"` + IOPS int64 `xml:"iops"` + Encrypted bool `xml:"encrypted"` + Tags []Tag `xml:"tagSet>item"` +} + +type VolumeAttachment struct { + VolumeId string `xml:"volumeId"` + InstanceId string `xml:"instanceId"` + Device string `xml:"device"` + Status string `xml:"status"` +} + +// Response to a DescribeVolumes request +type VolumesResp struct { + RequestId string `xml:"requestId"` + Volumes []Volume `xml:"volumeSet>item"` +} + +// Attach a volume. +func (ec2 *EC2) AttachVolume(volumeId string, instanceId string, device string) (resp *AttachVolumeResp, err error) { + params := makeParams("AttachVolume") + params["VolumeId"] = volumeId + params["InstanceId"] = instanceId + params["Device"] = device + + resp = &AttachVolumeResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Create a new volume. +func (ec2 *EC2) CreateVolume(options *CreateVolume) (resp *CreateVolumeResp, err error) { + params := makeParams("CreateVolume") + params["AvailabilityZone"] = options.AvailZone + if options.Size > 0 { + params["Size"] = strconv.FormatInt(options.Size, 10) + } + + if options.SnapshotId != "" { + params["SnapshotId"] = options.SnapshotId + } + + if options.VolumeType != "" { + params["VolumeType"] = options.VolumeType + } + + if options.IOPS > 0 { + params["Iops"] = strconv.FormatInt(options.IOPS, 10) + } + + if options.Encrypted { + params["Encrypted"] = "true" + } + + resp = &CreateVolumeResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Delete an EBS volume. +func (ec2 *EC2) DeleteVolume(id string) (resp *SimpleResp, err error) { + params := makeParams("DeleteVolume") + params["VolumeId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Detaches an EBS volume. +func (ec2 *EC2) DetachVolume(id string) (resp *SimpleResp, err error) { + params := makeParams("DetachVolume") + params["VolumeId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Finds or lists all volumes. +func (ec2 *EC2) Volumes(volIds []string, filter *Filter) (resp *VolumesResp, err error) { + params := makeParams("DescribeVolumes") + addParamsList(params, "VolumeId", volIds) + filter.addParams(params) + resp = &VolumesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ---------------------------------------------------------------------------- +// Availability zone management functions and types. +// See http://goo.gl/ylxT4R for more details. + +// DescribeAvailabilityZonesResp represents a response to a DescribeAvailabilityZones +// request in EC2. +type DescribeAvailabilityZonesResp struct { + RequestId string `xml:"requestId"` + Zones []AvailabilityZoneInfo `xml:"availabilityZoneInfo>item"` +} + +// AvailabilityZoneInfo encapsulates details for an availability zone in EC2. +type AvailabilityZoneInfo struct { + AvailabilityZone + State string `xml:"zoneState"` + MessageSet []string `xml:"messageSet>item"` +} + +// AvailabilityZone represents an EC2 availability zone. +type AvailabilityZone struct { + Name string `xml:"zoneName"` + Region string `xml:"regionName"` +} + +// DescribeAvailabilityZones returns details about availability zones in EC2. +// The filter parameter is optional, and if provided will limit the +// availability zones returned to those matching the given filtering +// rules. +// +// See http://goo.gl/ylxT4R for more details. +func (ec2 *EC2) DescribeAvailabilityZones(filter *Filter) (resp *DescribeAvailabilityZonesResp, err error) { + params := makeParams("DescribeAvailabilityZones") + filter.addParams(params) + resp = &DescribeAvailabilityZonesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ---------------------------------------------------------------------------- +// ElasticIp management (for VPC) + +// The AllocateAddress request parameters +// +// see http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-AllocateAddress.html +type AllocateAddress struct { + Domain string +} + +// Response to an AllocateAddress request +type AllocateAddressResp struct { + RequestId string `xml:"requestId"` + PublicIp string `xml:"publicIp"` + Domain string `xml:"domain"` + AllocationId string `xml:"allocationId"` +} + +// The AssociateAddress request parameters +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-AssociateAddress.html +type AssociateAddress struct { + InstanceId string + PublicIp string + AllocationId string + AllowReassociation bool +} + +// Response to an AssociateAddress request +type AssociateAddressResp struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` + AssociationId string `xml:"associationId"` +} + +// Address represents an Elastic IP Address +// See http://goo.gl/uxCjp7 for more details +type Address struct { + PublicIp string `xml:"publicIp"` + AllocationId string `xml:"allocationId"` + Domain string `xml:"domain"` + InstanceId string `xml:"instanceId"` + AssociationId string `xml:"associationId"` + NetworkInterfaceId string `xml:"networkInterfaceId"` + NetworkInterfaceOwnerId string `xml:"networkInterfaceOwnerId"` + PrivateIpAddress string `xml:"privateIpAddress"` +} + +type DescribeAddressesResp struct { + RequestId string `xml:"requestId"` + Addresses []Address `xml:"addressesSet>item"` +} + +// Allocate a new Elastic IP. +func (ec2 *EC2) AllocateAddress(options *AllocateAddress) (resp *AllocateAddressResp, err error) { + params := makeParams("AllocateAddress") + params["Domain"] = options.Domain + + resp = &AllocateAddressResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Release an Elastic IP (VPC). +func (ec2 *EC2) ReleaseAddress(id string) (resp *SimpleResp, err error) { + params := makeParams("ReleaseAddress") + params["AllocationId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Release an Elastic IP (Public) +func (ec2 *EC2) ReleasePublicAddress(publicIp string) (resp *SimpleResp, err error) { + params := makeParams("ReleaseAddress") + params["PublicIp"] = publicIp + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Associate an address with a VPC instance. +func (ec2 *EC2) AssociateAddress(options *AssociateAddress) (resp *AssociateAddressResp, err error) { + params := makeParams("AssociateAddress") + params["InstanceId"] = options.InstanceId + if options.PublicIp != "" { + params["PublicIp"] = options.PublicIp + } + if options.AllocationId != "" { + params["AllocationId"] = options.AllocationId + } + if options.AllowReassociation { + params["AllowReassociation"] = "true" + } + + resp = &AssociateAddressResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Disassociate an address from a VPC instance. +func (ec2 *EC2) DisassociateAddress(id string) (resp *SimpleResp, err error) { + params := makeParams("DisassociateAddress") + params["AssociationId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Disassociate an address from a VPC instance. +func (ec2 *EC2) DisassociateAddressClassic(ip string) (resp *SimpleResp, err error) { + params := makeParams("DisassociateAddress") + params["PublicIp"] = ip + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// DescribeAddresses returns details about one or more +// Elastic IP Addresses. Returned addresses can be +// filtered by Public IP, Allocation ID or multiple filters +// +// See http://goo.gl/zW7J4p for more details. +func (ec2 *EC2) Addresses(publicIps []string, allocationIds []string, filter *Filter) (resp *DescribeAddressesResp, err error) { + params := makeParams("DescribeAddresses") + addParamsList(params, "PublicIp", publicIps) + addParamsList(params, "AllocationId", allocationIds) + filter.addParams(params) + resp = &DescribeAddressesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ---------------------------------------------------------------------------- +// Image and snapshot management functions and types. + +// The CreateImage request parameters. +// +// See http://goo.gl/cxU41 for more details. +type CreateImage struct { + InstanceId string + Name string + Description string + NoReboot bool + BlockDevices []BlockDeviceMapping +} + +// Response to a CreateImage request. +// +// See http://goo.gl/cxU41 for more details. +type CreateImageResp struct { + RequestId string `xml:"requestId"` + ImageId string `xml:"imageId"` +} + +// Response to a DescribeImages request. +// +// See http://goo.gl/hLnyg for more details. +type ImagesResp struct { + RequestId string `xml:"requestId"` + Images []Image `xml:"imagesSet>item"` +} + +// Response to a DescribeImageAttribute request. +// +// See http://goo.gl/bHO3zT for more details. +type ImageAttributeResp struct { + RequestId string `xml:"requestId"` + ImageId string `xml:"imageId"` + Kernel string `xml:"kernel>value"` + RamDisk string `xml:"ramdisk>value"` + Description string `xml:"description>value"` + Group string `xml:"launchPermission>item>group"` + UserIds []string `xml:"launchPermission>item>userId"` + ProductCodes []string `xml:"productCodes>item>productCode"` + BlockDevices []BlockDeviceMapping `xml:"blockDeviceMapping>item"` +} + +// The RegisterImage request parameters. +type RegisterImage struct { + ImageLocation string + Name string + Description string + Architecture string + KernelId string + RamdiskId string + RootDeviceName string + VirtType string + SriovNetSupport string + BlockDevices []BlockDeviceMapping +} + +// Response to a RegisterImage request. +type RegisterImageResp struct { + RequestId string `xml:"requestId"` + ImageId string `xml:"imageId"` +} + +// Response to a DegisterImage request. +// +// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DeregisterImage.html +type DeregisterImageResp struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` +} + +// BlockDeviceMapping represents the association of a block device with an image. +// +// See http://goo.gl/wnDBf for more details. +type BlockDeviceMapping struct { + DeviceName string `xml:"deviceName"` + VirtualName string `xml:"virtualName"` + SnapshotId string `xml:"ebs>snapshotId"` + VolumeType string `xml:"ebs>volumeType"` + VolumeSize int64 `xml:"ebs>volumeSize"` + DeleteOnTermination bool `xml:"ebs>deleteOnTermination"` + Encrypted bool `xml:"ebs>encrypted"` + NoDevice bool `xml:"noDevice"` + + // The number of I/O operations per second (IOPS) that the volume supports. + IOPS int64 `xml:"ebs>iops"` +} + +// Image represents details about an image. +// +// See http://goo.gl/iSqJG for more details. +type Image struct { + Id string `xml:"imageId"` + Name string `xml:"name"` + Description string `xml:"description"` + Type string `xml:"imageType"` + State string `xml:"imageState"` + Location string `xml:"imageLocation"` + Public bool `xml:"isPublic"` + Architecture string `xml:"architecture"` + Platform string `xml:"platform"` + ProductCodes []string `xml:"productCode>item>productCode"` + KernelId string `xml:"kernelId"` + RamdiskId string `xml:"ramdiskId"` + StateReason string `xml:"stateReason"` + OwnerId string `xml:"imageOwnerId"` + OwnerAlias string `xml:"imageOwnerAlias"` + RootDeviceType string `xml:"rootDeviceType"` + RootDeviceName string `xml:"rootDeviceName"` + VirtualizationType string `xml:"virtualizationType"` + Hypervisor string `xml:"hypervisor"` + BlockDevices []BlockDeviceMapping `xml:"blockDeviceMapping>item"` + Tags []Tag `xml:"tagSet>item"` +} + +// The ModifyImageAttribute request parameters. +type ModifyImageAttribute struct { + AddUsers []string + RemoveUsers []string + AddGroups []string + RemoveGroups []string + ProductCodes []string + Description string +} + +// The CopyImage request parameters. +// +// See http://goo.gl/hQwPCK for more details. +type CopyImage struct { + SourceRegion string + SourceImageId string + Name string + Description string + ClientToken string +} + +// Response to a CopyImage request. +// +// See http://goo.gl/hQwPCK for more details. +type CopyImageResp struct { + RequestId string `xml:"requestId"` + ImageId string `xml:"imageId"` +} + +// Creates an Amazon EBS-backed AMI from an Amazon EBS-backed instance +// that is either running or stopped. +// +// See http://goo.gl/cxU41 for more details. +func (ec2 *EC2) CreateImage(options *CreateImage) (resp *CreateImageResp, err error) { + params := makeParams("CreateImage") + params["InstanceId"] = options.InstanceId + params["Name"] = options.Name + if options.Description != "" { + params["Description"] = options.Description + } + if options.NoReboot { + params["NoReboot"] = "true" + } + addBlockDeviceParams("", params, options.BlockDevices) + + resp = &CreateImageResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Images returns details about available images. +// The ids and filter parameters, if provided, will limit the images returned. +// For example, to get all the private images associated with this account set +// the boolean filter "is-public" to 0. +// For list of filters: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeImages.html +// +// Note: calling this function with nil ids and filter parameters will result in +// a very large number of images being returned. +// +// See http://goo.gl/SRBhW for more details. +func (ec2 *EC2) Images(ids []string, filter *Filter) (resp *ImagesResp, err error) { + params := makeParams("DescribeImages") + for i, id := range ids { + params["ImageId."+strconv.Itoa(i+1)] = id + } + filter.addParams(params) + + resp = &ImagesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ImagesByOwners returns details about available images. +// The ids, owners, and filter parameters, if provided, will limit the images returned. +// For example, to get all the private images associated with this account set +// the boolean filter "is-public" to 0. +// For list of filters: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeImages.html +// +// Note: calling this function with nil ids and filter parameters will result in +// a very large number of images being returned. +// +// See http://goo.gl/SRBhW for more details. +func (ec2 *EC2) ImagesByOwners(ids []string, owners []string, filter *Filter) (resp *ImagesResp, err error) { + params := makeParams("DescribeImages") + for i, id := range ids { + params["ImageId."+strconv.Itoa(i+1)] = id + } + for i, owner := range owners { + params[fmt.Sprintf("Owner.%d", i+1)] = owner + } + + filter.addParams(params) + + resp = &ImagesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ImageAttribute describes an attribute of an AMI. +// You can specify only one attribute at a time. +// Valid attributes are: +// description | kernel | ramdisk | launchPermission | productCodes | blockDeviceMapping +// +// See http://goo.gl/bHO3zT for more details. +func (ec2 *EC2) ImageAttribute(imageId, attribute string) (resp *ImageAttributeResp, err error) { + params := makeParams("DescribeImageAttribute") + params["ImageId"] = imageId + params["Attribute"] = attribute + + resp = &ImageAttributeResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ModifyImageAttribute sets attributes for an image. +// +// See http://goo.gl/YUjO4G for more details. +func (ec2 *EC2) ModifyImageAttribute(imageId string, options *ModifyImageAttribute) (resp *SimpleResp, err error) { + params := makeParams("ModifyImageAttribute") + params["ImageId"] = imageId + if options.Description != "" { + params["Description.Value"] = options.Description + } + + if options.AddUsers != nil { + for i, user := range options.AddUsers { + p := fmt.Sprintf("LaunchPermission.Add.%d.UserId", i+1) + params[p] = user + } + } + + if options.RemoveUsers != nil { + for i, user := range options.RemoveUsers { + p := fmt.Sprintf("LaunchPermission.Remove.%d.UserId", i+1) + params[p] = user + } + } + + if options.AddGroups != nil { + for i, group := range options.AddGroups { + p := fmt.Sprintf("LaunchPermission.Add.%d.Group", i+1) + params[p] = group + } + } + + if options.RemoveGroups != nil { + for i, group := range options.RemoveGroups { + p := fmt.Sprintf("LaunchPermission.Remove.%d.Group", i+1) + params[p] = group + } + } + + if options.ProductCodes != nil { + addParamsList(params, "ProductCode", options.ProductCodes) + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + resp = nil + } + + return +} + +// Registers a new AMI with EC2. +// +// See: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-RegisterImage.html +func (ec2 *EC2) RegisterImage(options *RegisterImage) (resp *RegisterImageResp, err error) { + params := makeParams("RegisterImage") + params["Name"] = options.Name + if options.ImageLocation != "" { + params["ImageLocation"] = options.ImageLocation + } + + if options.Description != "" { + params["Description"] = options.Description + } + + if options.Architecture != "" { + params["Architecture"] = options.Architecture + } + + if options.KernelId != "" { + params["KernelId"] = options.KernelId + } + + if options.RamdiskId != "" { + params["RamdiskId"] = options.RamdiskId + } + + if options.RootDeviceName != "" { + params["RootDeviceName"] = options.RootDeviceName + } + + if options.VirtType != "" { + params["VirtualizationType"] = options.VirtType + } + + if options.SriovNetSupport != "" { + params["SriovNetSupport"] = "simple" + } + + addBlockDeviceParams("", params, options.BlockDevices) + + resp = &RegisterImageResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Degisters an image. Note that this does not delete the backing stores of the AMI. +// +// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DeregisterImage.html +func (ec2 *EC2) DeregisterImage(imageId string) (resp *DeregisterImageResp, err error) { + params := makeParams("DeregisterImage") + params["ImageId"] = imageId + + resp = &DeregisterImageResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Copy and Image from one region to another. +// +// See http://goo.gl/hQwPCK for more details. +func (ec2 *EC2) CopyImage(options *CopyImage) (resp *CopyImageResp, err error) { + params := makeParams("CopyImage") + + if options.SourceRegion != "" { + params["SourceRegion"] = options.SourceRegion + } + + if options.SourceImageId != "" { + params["SourceImageId"] = options.SourceImageId + } + + if options.Name != "" { + params["Name"] = options.Name + } + + if options.Description != "" { + params["Description"] = options.Description + } + + if options.ClientToken != "" { + params["ClientToken"] = options.ClientToken + } + + resp = &CopyImageResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Response to a CreateSnapshot request. +// +// See http://goo.gl/ttcda for more details. +type CreateSnapshotResp struct { + RequestId string `xml:"requestId"` + Snapshot +} + +// CreateSnapshot creates a volume snapshot and stores it in S3. +// +// See http://goo.gl/ttcda for more details. +func (ec2 *EC2) CreateSnapshot(volumeId, description string) (resp *CreateSnapshotResp, err error) { + params := makeParams("CreateSnapshot") + params["VolumeId"] = volumeId + params["Description"] = description + + resp = &CreateSnapshotResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// DeleteSnapshots deletes the volume snapshots with the given ids. +// +// Note: If you make periodic snapshots of a volume, the snapshots are +// incremental so that only the blocks on the device that have changed +// since your last snapshot are incrementally saved in the new snapshot. +// Even though snapshots are saved incrementally, the snapshot deletion +// process is designed so that you need to retain only the most recent +// snapshot in order to restore the volume. +// +// See http://goo.gl/vwU1y for more details. +func (ec2 *EC2) DeleteSnapshots(ids []string) (resp *SimpleResp, err error) { + params := makeParams("DeleteSnapshot") + for i, id := range ids { + params["SnapshotId."+strconv.Itoa(i+1)] = id + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Response to a DescribeSnapshots request. +// +// See http://goo.gl/nClDT for more details. +type SnapshotsResp struct { + RequestId string `xml:"requestId"` + Snapshots []Snapshot `xml:"snapshotSet>item"` +} + +// Snapshot represents details about a volume snapshot. +// +// See http://goo.gl/nkovs for more details. +type Snapshot struct { + Id string `xml:"snapshotId"` + VolumeId string `xml:"volumeId"` + VolumeSize string `xml:"volumeSize"` + Status string `xml:"status"` + StartTime string `xml:"startTime"` + Description string `xml:"description"` + Progress string `xml:"progress"` + OwnerId string `xml:"ownerId"` + OwnerAlias string `xml:"ownerAlias"` + Encrypted bool `xml:"encrypted"` + Tags []Tag `xml:"tagSet>item"` +} + +// Snapshots returns details about volume snapshots available to the user. +// The ids and filter parameters, if provided, limit the snapshots returned. +// +// See http://goo.gl/ogJL4 for more details. +func (ec2 *EC2) Snapshots(ids []string, filter *Filter) (resp *SnapshotsResp, err error) { + params := makeParams("DescribeSnapshots") + for i, id := range ids { + params["SnapshotId."+strconv.Itoa(i+1)] = id + } + filter.addParams(params) + + resp = &SnapshotsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ---------------------------------------------------------------------------- +// KeyPair management functions and types. + +type KeyPair struct { + Name string `xml:"keyName"` + Fingerprint string `xml:"keyFingerprint"` +} + +type KeyPairsResp struct { + RequestId string `xml:"requestId"` + Keys []KeyPair `xml:"keySet>item"` +} + +type CreateKeyPairResp struct { + RequestId string `xml:"requestId"` + KeyName string `xml:"keyName"` + KeyFingerprint string `xml:"keyFingerprint"` + KeyMaterial string `xml:"keyMaterial"` +} + +type ImportKeyPairResponse struct { + RequestId string `xml:"requestId"` + KeyName string `xml:"keyName"` + KeyFingerprint string `xml:"keyFingerprint"` +} + +// CreateKeyPair creates a new key pair and returns the private key contents. +// +// See http://goo.gl/0S6hV +func (ec2 *EC2) CreateKeyPair(keyName string) (resp *CreateKeyPairResp, err error) { + params := makeParams("CreateKeyPair") + params["KeyName"] = keyName + + resp = &CreateKeyPairResp{} + err = ec2.query(params, resp) + if err == nil { + resp.KeyFingerprint = strings.TrimSpace(resp.KeyFingerprint) + } + return +} + +// DeleteKeyPair deletes a key pair. +// +// See http://goo.gl/0bqok +func (ec2 *EC2) DeleteKeyPair(name string) (resp *SimpleResp, err error) { + params := makeParams("DeleteKeyPair") + params["KeyName"] = name + + resp = &SimpleResp{} + err = ec2.query(params, resp) + return +} + +// KeyPairs returns list of key pairs for this account +// +// See http://goo.gl/Apzsfz +func (ec2 *EC2) KeyPairs(keynames []string, filter *Filter) (resp *KeyPairsResp, err error) { + params := makeParams("DescribeKeyPairs") + for i, name := range keynames { + params["KeyName."+strconv.Itoa(i)] = name + } + filter.addParams(params) + + resp = &KeyPairsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +// ImportKeyPair imports a key into AWS +// +// See http://goo.gl/NbZUvw +func (ec2 *EC2) ImportKeyPair(keyname string, key string) (resp *ImportKeyPairResponse, err error) { + params := makeParams("ImportKeyPair") + params["KeyName"] = keyname + + // Oddly, AWS requires the key material to be base64-encoded, even if it was + // already encoded. So, we force another round of encoding... + // c.f. https://groups.google.com/forum/?fromgroups#!topic/boto-dev/IczrStO9Q8M + params["PublicKeyMaterial"] = base64.StdEncoding.EncodeToString([]byte(key)) + + resp = &ImportKeyPairResponse{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// ---------------------------------------------------------------------------- +// Security group management functions and types. + +// SimpleResp represents a response to an EC2 request which on success will +// return no other information besides a request id. +type SimpleResp struct { + XMLName xml.Name + RequestId string `xml:"requestId"` +} + +// CreateSecurityGroupResp represents a response to a CreateSecurityGroup request. +type CreateSecurityGroupResp struct { + SecurityGroup + RequestId string `xml:"requestId"` +} + +// CreateSecurityGroup run a CreateSecurityGroup request in EC2, with the provided +// name and description. +// +// See http://goo.gl/Eo7Yl for more details. +func (ec2 *EC2) CreateSecurityGroup(group SecurityGroup) (resp *CreateSecurityGroupResp, err error) { + params := makeParams("CreateSecurityGroup") + params["GroupName"] = group.Name + params["GroupDescription"] = group.Description + if group.VpcId != "" { + params["VpcId"] = group.VpcId + } + + resp = &CreateSecurityGroupResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + resp.Name = group.Name + return resp, nil +} + +// SecurityGroupsResp represents a response to a DescribeSecurityGroups +// request in EC2. +// +// See http://goo.gl/k12Uy for more details. +type SecurityGroupsResp struct { + RequestId string `xml:"requestId"` + Groups []SecurityGroupInfo `xml:"securityGroupInfo>item"` +} + +// SecurityGroup encapsulates details for a security group in EC2. +// +// See http://goo.gl/CIdyP for more details. +type SecurityGroupInfo struct { + SecurityGroup + OwnerId string `xml:"ownerId"` + Description string `xml:"groupDescription"` + IPPerms []IPPerm `xml:"ipPermissions>item"` +} + +// IPPerm represents an allowance within an EC2 security group. +// +// See http://goo.gl/4oTxv for more details. +type IPPerm struct { + Protocol string `xml:"ipProtocol"` + FromPort int `xml:"fromPort"` + ToPort int `xml:"toPort"` + SourceIPs []string `xml:"ipRanges>item>cidrIp"` + SourceGroups []UserSecurityGroup `xml:"groups>item"` +} + +// UserSecurityGroup holds a security group and the owner +// of that group. +type UserSecurityGroup struct { + Id string `xml:"groupId"` + Name string `xml:"groupName"` + OwnerId string `xml:"userId"` +} + +// SecurityGroup represents an EC2 security group. +// If SecurityGroup is used as a parameter, then one of Id or Name +// may be empty. If both are set, then Id is used. +type SecurityGroup struct { + Id string `xml:"groupId"` + Name string `xml:"groupName"` + Description string `xml:"groupDescription"` + VpcId string `xml:"vpcId"` + Tags []Tag `xml:"tagSet>item"` +} + +// SecurityGroupNames is a convenience function that +// returns a slice of security groups with the given names. +func SecurityGroupNames(names ...string) []SecurityGroup { + g := make([]SecurityGroup, len(names)) + for i, name := range names { + g[i] = SecurityGroup{Name: name} + } + return g +} + +// SecurityGroupNames is a convenience function that +// returns a slice of security groups with the given ids. +func SecurityGroupIds(ids ...string) []SecurityGroup { + g := make([]SecurityGroup, len(ids)) + for i, id := range ids { + g[i] = SecurityGroup{Id: id} + } + return g +} + +// SecurityGroups returns details about security groups in EC2. Both parameters +// are optional, and if provided will limit the security groups returned to those +// matching the given groups or filtering rules. +// +// See http://goo.gl/k12Uy for more details. +func (ec2 *EC2) SecurityGroups(groups []SecurityGroup, filter *Filter) (resp *SecurityGroupsResp, err error) { + params := makeParams("DescribeSecurityGroups") + i, j := 1, 1 + for _, g := range groups { + if g.Id != "" { + params["GroupId."+strconv.Itoa(i)] = g.Id + i++ + } else { + params["GroupName."+strconv.Itoa(j)] = g.Name + j++ + } + } + filter.addParams(params) + + resp = &SecurityGroupsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// DeleteSecurityGroup removes the given security group in EC2. +// +// See http://goo.gl/QJJDO for more details. +func (ec2 *EC2) DeleteSecurityGroup(group SecurityGroup) (resp *SimpleResp, err error) { + params := makeParams("DeleteSecurityGroup") + if group.Id != "" { + params["GroupId"] = group.Id + } else { + params["GroupName"] = group.Name + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// AuthorizeSecurityGroup creates an allowance for clients matching the provided +// rules to access instances within the given security group. +// +// See http://goo.gl/u2sDJ for more details. +func (ec2 *EC2) AuthorizeSecurityGroup(group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) { + return ec2.authOrRevoke("AuthorizeSecurityGroupIngress", group, perms) +} + +// AuthorizeSecurityGroupEgress creates an allowance for clients matching the provided +// rules for egress access. +// +// See http://goo.gl/UHnH4L for more details. +func (ec2 *EC2) AuthorizeSecurityGroupEgress(group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) { + return ec2.authOrRevoke("AuthorizeSecurityGroupEgress", group, perms) +} + +// RevokeSecurityGroup revokes permissions from a group. +// +// See http://goo.gl/ZgdxA for more details. +func (ec2 *EC2) RevokeSecurityGroup(group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) { + return ec2.authOrRevoke("RevokeSecurityGroupIngress", group, perms) +} + +func (ec2 *EC2) authOrRevoke(op string, group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) { + params := makeParams(op) + if group.Id != "" { + params["GroupId"] = group.Id + } else { + params["GroupName"] = group.Name + } + + for i, perm := range perms { + prefix := "IpPermissions." + strconv.Itoa(i+1) + params[prefix+".IpProtocol"] = perm.Protocol + params[prefix+".FromPort"] = strconv.Itoa(perm.FromPort) + params[prefix+".ToPort"] = strconv.Itoa(perm.ToPort) + for j, ip := range perm.SourceIPs { + params[prefix+".IpRanges."+strconv.Itoa(j+1)+".CidrIp"] = ip + } + for j, g := range perm.SourceGroups { + subprefix := prefix + ".Groups." + strconv.Itoa(j+1) + if g.OwnerId != "" { + params[subprefix+".UserId"] = g.OwnerId + } + if g.Id != "" { + params[subprefix+".GroupId"] = g.Id + } else { + params[subprefix+".GroupName"] = g.Name + } + } + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// ResourceTag represents key-value metadata used to classify and organize +// EC2 instances. +// +// See http://goo.gl/bncl3 for more details +type Tag struct { + Key string `xml:"key"` + Value string `xml:"value"` +} + +// CreateTags adds or overwrites one or more tags for the specified taggable resources. +// For a list of tagable resources, see: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html +// +// See http://goo.gl/Vmkqc for more details +func (ec2 *EC2) CreateTags(resourceIds []string, tags []Tag) (resp *SimpleResp, err error) { + params := makeParams("CreateTags") + addParamsList(params, "ResourceId", resourceIds) + + for j, tag := range tags { + params["Tag."+strconv.Itoa(j+1)+".Key"] = tag.Key + params["Tag."+strconv.Itoa(j+1)+".Value"] = tag.Value + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// DeleteTags deletes tags. +func (ec2 *EC2) DeleteTags(resourceIds []string, tags []Tag) (resp *SimpleResp, err error) { + params := makeParams("DeleteTags") + addParamsList(params, "ResourceId", resourceIds) + + for j, tag := range tags { + params["Tag."+strconv.Itoa(j+1)+".Key"] = tag.Key + + if tag.Value != "" { + params["Tag."+strconv.Itoa(j+1)+".Value"] = tag.Value + } + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +type TagsResp struct { + RequestId string `xml:"requestId"` + Tags []ResourceTag `xml:"tagSet>item"` +} + +type ResourceTag struct { + Tag + ResourceId string `xml:"resourceId"` + ResourceType string `xml:"resourceType"` +} + +func (ec2 *EC2) Tags(filter *Filter) (*TagsResp, error) { + params := makeParams("DescribeTags") + filter.addParams(params) + + resp := &TagsResp{} + if err := ec2.query(params, resp); err != nil { + return nil, err + } + + return resp, nil +} + +// Response to a StartInstances request. +// +// See http://goo.gl/awKeF for more details. +type StartInstanceResp struct { + RequestId string `xml:"requestId"` + StateChanges []InstanceStateChange `xml:"instancesSet>item"` +} + +// Response to a StopInstances request. +// +// See http://goo.gl/436dJ for more details. +type StopInstanceResp struct { + RequestId string `xml:"requestId"` + StateChanges []InstanceStateChange `xml:"instancesSet>item"` +} + +// StartInstances starts an Amazon EBS-backed AMI that you've previously stopped. +// +// See http://goo.gl/awKeF for more details. +func (ec2 *EC2) StartInstances(ids ...string) (resp *StartInstanceResp, err error) { + params := makeParams("StartInstances") + addParamsList(params, "InstanceId", ids) + resp = &StartInstanceResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// StopInstances requests stopping one or more Amazon EBS-backed instances. +// +// See http://goo.gl/436dJ for more details. +func (ec2 *EC2) StopInstances(ids ...string) (resp *StopInstanceResp, err error) { + params := makeParams("StopInstances") + addParamsList(params, "InstanceId", ids) + resp = &StopInstanceResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// RebootInstance requests a reboot of one or more instances. This operation is asynchronous; +// it only queues a request to reboot the specified instance(s). The operation will succeed +// if the instances are valid and belong to you. +// +// Requests to reboot terminated instances are ignored. +// +// See http://goo.gl/baoUf for more details. +func (ec2 *EC2) RebootInstances(ids ...string) (resp *SimpleResp, err error) { + params := makeParams("RebootInstances") + addParamsList(params, "InstanceId", ids) + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// The ModifyInstanceAttribute request parameters. +type ModifyInstance struct { + InstanceType string + BlockDevices []BlockDeviceMapping + DisableAPITermination bool + EbsOptimized bool + SecurityGroups []SecurityGroup + ShutdownBehavior string + KernelId string + RamdiskId string + SourceDestCheck bool + SriovNetSupport bool + UserData []byte + + SetSourceDestCheck bool +} + +// Response to a ModifyInstanceAttribute request. +// +// http://goo.gl/icuXh5 for more details. +type ModifyInstanceResp struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` +} + +// ModifyImageAttribute modifies the specified attribute of the specified instance. +// You can specify only one attribute at a time. To modify some attributes, the +// instance must be stopped. +// +// See http://goo.gl/icuXh5 for more details. +func (ec2 *EC2) ModifyInstance(instId string, options *ModifyInstance) (resp *ModifyInstanceResp, err error) { + params := makeParams("ModifyInstanceAttribute") + params["InstanceId"] = instId + addBlockDeviceParams("", params, options.BlockDevices) + + if options.InstanceType != "" { + params["InstanceType.Value"] = options.InstanceType + } + + if options.DisableAPITermination { + params["DisableApiTermination.Value"] = "true" + } + + if options.EbsOptimized { + params["EbsOptimized"] = "true" + } + + if options.ShutdownBehavior != "" { + params["InstanceInitiatedShutdownBehavior.Value"] = options.ShutdownBehavior + } + + if options.KernelId != "" { + params["Kernel.Value"] = options.KernelId + } + + if options.RamdiskId != "" { + params["Ramdisk.Value"] = options.RamdiskId + } + + if options.SourceDestCheck || options.SetSourceDestCheck { + if options.SourceDestCheck { + params["SourceDestCheck.Value"] = "true" + } else { + params["SourceDestCheck.Value"] = "false" + } + } + + if options.SriovNetSupport { + params["SriovNetSupport.Value"] = "simple" + } + + if options.UserData != nil { + userData := make([]byte, b64.EncodedLen(len(options.UserData))) + b64.Encode(userData, options.UserData) + params["UserData"] = string(userData) + } + + i := 1 + for _, g := range options.SecurityGroups { + if g.Id != "" { + params["GroupId."+strconv.Itoa(i)] = g.Id + i++ + } + } + + resp = &ModifyInstanceResp{} + err = ec2.query(params, resp) + if err != nil { + resp = nil + } + return +} + +// ---------------------------------------------------------------------------- +// VPC management functions and types. + +// The CreateVpc request parameters +// +// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateVpc.html +type CreateVpc struct { + CidrBlock string + InstanceTenancy string +} + +// Response to a CreateVpc request +type CreateVpcResp struct { + RequestId string `xml:"requestId"` + VPC VPC `xml:"vpc"` +} + +// The ModifyVpcAttribute request parameters. +// +// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVpcAttribute.html for more details. +type ModifyVpcAttribute struct { + EnableDnsSupport bool + EnableDnsHostnames bool + + SetEnableDnsSupport bool + SetEnableDnsHostnames bool +} + +// Response to a DescribeVpcAttribute request. +// +// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVpcAttribute.html for more details. +type VpcAttributeResp struct { + RequestId string `xml:"requestId"` + VpcId string `xml:"vpcId"` + EnableDnsSupport bool `xml:"enableDnsSupport>value"` + EnableDnsHostnames bool `xml:"enableDnsHostnames>value"` +} + +// CreateInternetGateway request parameters. +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateInternetGateway.html +type CreateInternetGateway struct{} + +// CreateInternetGateway response +type CreateInternetGatewayResp struct { + RequestId string `xml:"requestId"` + InternetGateway InternetGateway `xml:"internetGateway"` +} + +// The CreateRouteTable request parameters. +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateRouteTable.html +type CreateRouteTable struct { + VpcId string +} + +// Response to a CreateRouteTable request. +type CreateRouteTableResp struct { + RequestId string `xml:"requestId"` + RouteTable RouteTable `xml:"routeTable"` +} + +// CreateRoute request parameters +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateRoute.html +type CreateRoute struct { + RouteTableId string + DestinationCidrBlock string + GatewayId string + InstanceId string + NetworkInterfaceId string + VpcPeeringConnectionId string +} +type ReplaceRoute struct { + RouteTableId string + DestinationCidrBlock string + GatewayId string + InstanceId string + NetworkInterfaceId string + VpcPeeringConnectionId string +} + +type AssociateRouteTableResp struct { + RequestId string `xml:"requestId"` + AssociationId string `xml:"associationId"` +} +type ReassociateRouteTableResp struct { + RequestId string `xml:"requestId"` + AssociationId string `xml:"newAssociationId"` +} + +// The CreateSubnet request parameters +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateSubnet.html +type CreateSubnet struct { + VpcId string + CidrBlock string + AvailabilityZone string +} + +// Response to a CreateSubnet request +type CreateSubnetResp struct { + RequestId string `xml:"requestId"` + Subnet Subnet `xml:"subnet"` +} + +// The ModifySubnetAttribute request parameters +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-ModifySubnetAttribute.html +type ModifySubnetAttribute struct { + SubnetId string + MapPublicIpOnLaunch bool +} + +type ModifySubnetAttributeResp struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` +} + +// The CreateNetworkAcl request parameters +// +// http://goo.gl/BZmCRF +type CreateNetworkAcl struct { + VpcId string +} + +// Response to a CreateNetworkAcl request +type CreateNetworkAclResp struct { + RequestId string `xml:"requestId"` + NetworkAcl NetworkAcl `xml:"networkAcl"` +} + +// Response to CreateNetworkAclEntry request +type CreateNetworkAclEntryResp struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` +} + +// Response to a DescribeInternetGateways request. +type InternetGatewaysResp struct { + RequestId string `xml:"requestId"` + InternetGateways []InternetGateway `xml:"internetGatewaySet>item"` +} + +// Response to a DescribeRouteTables request. +type RouteTablesResp struct { + RequestId string `xml:"requestId"` + RouteTables []RouteTable `xml:"routeTableSet>item"` +} + +// Response to a DescribeVpcs request. +type VpcsResp struct { + RequestId string `xml:"requestId"` + VPCs []VPC `xml:"vpcSet>item"` +} + +// Internet Gateway +type InternetGateway struct { + InternetGatewayId string `xml:"internetGatewayId"` + Attachments []InternetGatewayAttachment `xml:"attachmentSet>item"` + Tags []Tag `xml:"tagSet>item"` +} + +type InternetGatewayAttachment struct { + VpcId string `xml:"vpcId"` + State string `xml:"state"` +} + +// Routing Table +type RouteTable struct { + RouteTableId string `xml:"routeTableId"` + VpcId string `xml:"vpcId"` + Associations []RouteTableAssociation `xml:"associationSet>item"` + Routes []Route `xml:"routeSet>item"` + Tags []Tag `xml:"tagSet>item"` +} + +type RouteTableAssociation struct { + AssociationId string `xml:"routeTableAssociationId"` + RouteTableId string `xml:"routeTableId"` + SubnetId string `xml:"subnetId"` + Main bool `xml:"main"` +} + +type Route struct { + DestinationCidrBlock string `xml:"destinationCidrBlock"` + GatewayId string `xml:"gatewayId"` + InstanceId string `xml:"instanceId"` + InstanceOwnerId string `xml:"instanceOwnerId"` + NetworkInterfaceId string `xml:"networkInterfaceId"` + State string `xml:"state"` + Origin string `xml:"origin"` + VpcPeeringConnectionId string `xml:"vpcPeeringConnectionId"` +} + +// Subnet +type Subnet struct { + SubnetId string `xml:"subnetId"` + State string `xml:"state"` + VpcId string `xml:"vpcId"` + CidrBlock string `xml:"cidrBlock"` + AvailableIpAddressCount int `xml:"availableIpAddressCount"` + AvailabilityZone string `xml:"availabilityZone"` + DefaultForAZ bool `xml:"defaultForAz"` + MapPublicIpOnLaunch bool `xml:"mapPublicIpOnLaunch"` + Tags []Tag `xml:"tagSet>item"` +} + +// NetworkAcl represent network acl +type NetworkAcl struct { + NetworkAclId string `xml:"networkAclId"` + VpcId string `xml:"vpcId"` + Default string `xml:"default"` + EntrySet []NetworkAclEntry `xml:"entrySet>item"` + AssociationSet []NetworkAclAssociation `xml:"associationSet>item"` + Tags []Tag `xml:"tagSet>item"` +} + +// NetworkAclAssociation +type NetworkAclAssociation struct { + NetworkAclAssociationId string `xml:"networkAclAssociationId"` + NetworkAclId string `xml:"networkAclId"` + SubnetId string `xml:"subnetId"` +} + +// NetworkAclEntry represent a rule within NetworkAcl +type NetworkAclEntry struct { + RuleNumber int `xml:"ruleNumber"` + Protocol int `xml:"protocol"` + RuleAction string `xml:"ruleAction"` + Egress bool `xml:"egress"` + CidrBlock string `xml:"cidrBlock"` + IcmpCode IcmpCode `xml:"icmpTypeCode"` + PortRange PortRange `xml:"portRange"` +} + +// IcmpCode +type IcmpCode struct { + Code int `xml:"code"` + Type int `xml:"type"` +} + +// PortRange +type PortRange struct { + From int `xml:"from"` + To int `xml:"to"` +} + +// Response to describe NetworkAcls +type NetworkAclsResp struct { + RequestId string `xml:"requestId"` + NetworkAcls []NetworkAcl `xml:"networkAclSet>item"` +} + +// VPC represents a single VPC. +type VPC struct { + VpcId string `xml:"vpcId"` + State string `xml:"state"` + CidrBlock string `xml:"cidrBlock"` + DHCPOptionsID string `xml:"dhcpOptionsId"` + InstanceTenancy string `xml:"instanceTenancy"` + IsDefault bool `xml:"isDefault"` + Tags []Tag `xml:"tagSet>item"` +} + +// Response to a DescribeSubnets request. +type SubnetsResp struct { + RequestId string `xml:"requestId"` + Subnets []Subnet `xml:"subnetSet>item"` +} + +// Create a new VPC. +func (ec2 *EC2) CreateVpc(options *CreateVpc) (resp *CreateVpcResp, err error) { + params := makeParams("CreateVpc") + params["CidrBlock"] = options.CidrBlock + + if options.InstanceTenancy != "" { + params["InstanceTenancy"] = options.InstanceTenancy + } + + resp = &CreateVpcResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Delete a VPC. +func (ec2 *EC2) DeleteVpc(id string) (resp *SimpleResp, err error) { + params := makeParams("DeleteVpc") + params["VpcId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// DescribeVpcs +// +// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeVpcs.html +func (ec2 *EC2) DescribeVpcs(ids []string, filter *Filter) (resp *VpcsResp, err error) { + params := makeParams("DescribeVpcs") + addParamsList(params, "VpcId", ids) + filter.addParams(params) + resp = &VpcsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// VpcAttribute describes an attribute of a VPC. +// You can specify only one attribute at a time. +// Valid attributes are: +// enableDnsSupport | enableDnsHostnames +// +// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVpcAttribute.html for more details. +func (ec2 *EC2) VpcAttribute(vpcId, attribute string) (resp *VpcAttributeResp, err error) { + params := makeParams("DescribeVpcAttribute") + params["VpcId"] = vpcId + params["Attribute"] = attribute + + resp = &VpcAttributeResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ModifyVpcAttribute modifies the specified attribute of the specified VPC. +// +// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-ModifyVpcAttribute.html for more details. +func (ec2 *EC2) ModifyVpcAttribute(vpcId string, options *ModifyVpcAttribute) (*SimpleResp, error) { + params := makeParams("ModifyVpcAttribute") + + params["VpcId"] = vpcId + + if options.SetEnableDnsSupport { + params["EnableDnsSupport.Value"] = strconv.FormatBool(options.EnableDnsSupport) + } + + if options.SetEnableDnsHostnames { + params["EnableDnsHostnames.Value"] = strconv.FormatBool(options.EnableDnsHostnames) + } + + resp := &SimpleResp{} + if err := ec2.query(params, resp); err != nil { + return nil, err + } + + return resp, nil +} + +// Create a new subnet. +func (ec2 *EC2) CreateSubnet(options *CreateSubnet) (resp *CreateSubnetResp, err error) { + params := makeParams("CreateSubnet") + params["AvailabilityZone"] = options.AvailabilityZone + params["CidrBlock"] = options.CidrBlock + params["VpcId"] = options.VpcId + + resp = &CreateSubnetResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Delete a Subnet. +func (ec2 *EC2) DeleteSubnet(id string) (resp *SimpleResp, err error) { + params := makeParams("DeleteSubnet") + params["SubnetId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ModifySubnetAttribute +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-ModifySubnetAttribute.html +func (ec2 *EC2) ModifySubnetAttribute(options *ModifySubnetAttribute) (resp *ModifySubnetAttributeResp, err error) { + params := makeParams("ModifySubnetAttribute") + params["SubnetId"] = options.SubnetId + if options.MapPublicIpOnLaunch { + params["MapPublicIpOnLaunch.Value"] = "true" + } else { + params["MapPublicIpOnLaunch.Value"] = "false" + } + + resp = &ModifySubnetAttributeResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// DescribeSubnets +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeSubnets.html +func (ec2 *EC2) DescribeSubnets(ids []string, filter *Filter) (resp *SubnetsResp, err error) { + params := makeParams("DescribeSubnets") + addParamsList(params, "SubnetId", ids) + filter.addParams(params) + + resp = &SubnetsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// CreateNetworkAcl creates a network ACL in a VPC. +// +// http://goo.gl/51X7db +func (ec2 *EC2) CreateNetworkAcl(options *CreateNetworkAcl) (resp *CreateNetworkAclResp, err error) { + params := makeParams("CreateNetworkAcl") + params["VpcId"] = options.VpcId + + resp = &CreateNetworkAclResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// CreateNetworkAclEntry creates an entry (a rule) in a network ACL with the specified rule number. +// +// http://goo.gl/BtXhtj +func (ec2 *EC2) CreateNetworkAclEntry(networkAclId string, options *NetworkAclEntry) (resp *CreateNetworkAclEntryResp, err error) { + + params := makeParams("CreateNetworkAclEntry") + params["NetworkAclId"] = networkAclId + params["RuleNumber"] = strconv.Itoa(options.RuleNumber) + params["Protocol"] = strconv.Itoa(options.Protocol) + params["RuleAction"] = options.RuleAction + params["Egress"] = strconv.FormatBool(options.Egress) + params["CidrBlock"] = options.CidrBlock + if params["Protocol"] == "-1" { + params["Icmp.Type"] = strconv.Itoa(options.IcmpCode.Type) + params["Icmp.Code"] = strconv.Itoa(options.IcmpCode.Code) + } + params["PortRange.From"] = strconv.Itoa(options.PortRange.From) + params["PortRange.To"] = strconv.Itoa(options.PortRange.To) + + resp = &CreateNetworkAclEntryResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +// NetworkAcls describes one or more of your network ACLs for given filter. +// +// http://goo.gl/mk9RsV +func (ec2 *EC2) NetworkAcls(networkAclIds []string, filter *Filter) (resp *NetworkAclsResp, err error) { + params := makeParams("DescribeNetworkAcls") + addParamsList(params, "NetworkAclId", networkAclIds) + filter.addParams(params) + resp = &NetworkAclsResp{} + if err = ec2.query(params, resp); err != nil { + return nil, err + } + + return resp, nil +} + +// Response to a DeleteNetworkAcl request. +type DeleteNetworkAclResp struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` +} + +// DeleteNetworkAcl deletes the network ACL with specified id. +// +// http://goo.gl/nC78Wx +func (ec2 *EC2) DeleteNetworkAcl(id string) (resp *DeleteNetworkAclResp, err error) { + params := makeParams("DeleteNetworkAcl") + params["NetworkAclId"] = id + + resp = &DeleteNetworkAclResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// Response to a DeleteNetworkAclEntry request. +type DeleteNetworkAclEntryResp struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` +} + +// DeleteNetworkAclEntry deletes the specified ingress or egress entry (rule) from the specified network ACL. +// +// http://goo.gl/moQbE2 +func (ec2 *EC2) DeleteNetworkAclEntry(id string, ruleNumber int, egress bool) (resp *DeleteNetworkAclEntryResp, err error) { + params := makeParams("DeleteNetworkAclEntry") + params["NetworkAclId"] = id + params["RuleNumber"] = strconv.Itoa(ruleNumber) + params["Egress"] = strconv.FormatBool(egress) + + resp = &DeleteNetworkAclEntryResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +type ReplaceNetworkAclAssociationResponse struct { + RequestId string `xml:"requestId"` + NewAssociationId string `xml:"newAssociationId"` +} + +// ReplaceNetworkAclAssociation changes which network ACL a subnet is associated with. +// +// http://goo.gl/ar0MH5 +func (ec2 *EC2) ReplaceNetworkAclAssociation(associationId string, networkAclId string) (resp *ReplaceNetworkAclAssociationResponse, err error) { + params := makeParams("ReplaceNetworkAclAssociation") + params["NetworkAclId"] = networkAclId + params["AssociationId"] = associationId + + resp = &ReplaceNetworkAclAssociationResponse{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// Create a new internet gateway. +func (ec2 *EC2) CreateInternetGateway( + options *CreateInternetGateway) (resp *CreateInternetGatewayResp, err error) { + params := makeParams("CreateInternetGateway") + + resp = &CreateInternetGatewayResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Attach an InternetGateway. +func (ec2 *EC2) AttachInternetGateway(id, vpcId string) (resp *SimpleResp, err error) { + params := makeParams("AttachInternetGateway") + params["InternetGatewayId"] = id + params["VpcId"] = vpcId + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Detach an InternetGateway. +func (ec2 *EC2) DetachInternetGateway(id, vpcId string) (resp *SimpleResp, err error) { + params := makeParams("DetachInternetGateway") + params["InternetGatewayId"] = id + params["VpcId"] = vpcId + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Delete an InternetGateway. +func (ec2 *EC2) DeleteInternetGateway(id string) (resp *SimpleResp, err error) { + params := makeParams("DeleteInternetGateway") + params["InternetGatewayId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// DescribeInternetGateways +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeInternetGateways.html +func (ec2 *EC2) DescribeInternetGateways(ids []string, filter *Filter) (resp *InternetGatewaysResp, err error) { + params := makeParams("DescribeInternetGateways") + addParamsList(params, "InternetGatewayId", ids) + filter.addParams(params) + + resp = &InternetGatewaysResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Create a new routing table. +func (ec2 *EC2) CreateRouteTable( + options *CreateRouteTable) (resp *CreateRouteTableResp, err error) { + params := makeParams("CreateRouteTable") + params["VpcId"] = options.VpcId + + resp = &CreateRouteTableResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Delete a RouteTable. +func (ec2 *EC2) DeleteRouteTable(id string) (resp *SimpleResp, err error) { + params := makeParams("DeleteRouteTable") + params["RouteTableId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// DescribeRouteTables +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeRouteTables.html +func (ec2 *EC2) DescribeRouteTables(ids []string, filter *Filter) (resp *RouteTablesResp, err error) { + params := makeParams("DescribeRouteTables") + addParamsList(params, "RouteTableId", ids) + filter.addParams(params) + + resp = &RouteTablesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Associate a routing table. +func (ec2 *EC2) AssociateRouteTable(id, subnetId string) (*AssociateRouteTableResp, error) { + params := makeParams("AssociateRouteTable") + params["RouteTableId"] = id + params["SubnetId"] = subnetId + + resp := &AssociateRouteTableResp{} + err := ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// Disassociate a routing table. +func (ec2 *EC2) DisassociateRouteTable(id string) (*SimpleResp, error) { + params := makeParams("DisassociateRouteTable") + params["AssociationId"] = id + + resp := &SimpleResp{} + err := ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// Re-associate a routing table. +func (ec2 *EC2) ReassociateRouteTable(id, routeTableId string) (*ReassociateRouteTableResp, error) { + params := makeParams("ReplaceRouteTableAssociation") + params["AssociationId"] = id + params["RouteTableId"] = routeTableId + + resp := &ReassociateRouteTableResp{} + err := ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// Create a new route. +func (ec2 *EC2) CreateRoute(options *CreateRoute) (resp *SimpleResp, err error) { + params := makeParams("CreateRoute") + params["RouteTableId"] = options.RouteTableId + params["DestinationCidrBlock"] = options.DestinationCidrBlock + + if v := options.GatewayId; v != "" { + params["GatewayId"] = v + } + if v := options.InstanceId; v != "" { + params["InstanceId"] = v + } + if v := options.NetworkInterfaceId; v != "" { + params["NetworkInterfaceId"] = v + } + if v := options.VpcPeeringConnectionId; v != "" { + params["VpcPeeringConnectionId"] = v + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Delete a Route. +func (ec2 *EC2) DeleteRoute(routeTableId, cidr string) (resp *SimpleResp, err error) { + params := makeParams("DeleteRoute") + params["RouteTableId"] = routeTableId + params["DestinationCidrBlock"] = cidr + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Replace a new route. +func (ec2 *EC2) ReplaceRoute(options *ReplaceRoute) (resp *SimpleResp, err error) { + params := makeParams("ReplaceRoute") + params["RouteTableId"] = options.RouteTableId + params["DestinationCidrBlock"] = options.DestinationCidrBlock + + if v := options.GatewayId; v != "" { + params["GatewayId"] = v + } + if v := options.InstanceId; v != "" { + params["InstanceId"] = v + } + if v := options.NetworkInterfaceId; v != "" { + params["NetworkInterfaceId"] = v + } + if v := options.VpcPeeringConnectionId; v != "" { + params["VpcPeeringConnectionId"] = v + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// The ResetImageAttribute request parameters. +type ResetImageAttribute struct { + Attribute string +} + +// ResetImageAttribute resets an attribute of an AMI to its default value. +// +// http://goo.gl/r6ZCPm for more details. +func (ec2 *EC2) ResetImageAttribute(imageId string, options *ResetImageAttribute) (resp *SimpleResp, err error) { + params := makeParams("ResetImageAttribute") + params["ImageId"] = imageId + + if options.Attribute != "" { + params["Attribute"] = options.Attribute + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2_test.go new file mode 100644 index 000000000000..3ea2bdc75ed5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2_test.go @@ -0,0 +1,1438 @@ +package ec2_test + +import ( + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/goamz/testutil" + . "github.com/motain/gocheck" + "testing" +) + +func Test(t *testing.T) { + TestingT(t) +} + +var _ = Suite(&S{}) + +type S struct { + ec2 *ec2.EC2 +} + +var testServer = testutil.NewHTTPServer() + +func (s *S) SetUpSuite(c *C) { + testServer.Start() + auth := aws.Auth{"abc", "123", ""} + s.ec2 = ec2.NewWithClient( + auth, + aws.Region{EC2Endpoint: testServer.URL}, + testutil.DefaultClient, + ) +} + +func (s *S) TearDownTest(c *C) { + testServer.Flush() +} + +func (s *S) TestRunInstancesErrorDump(c *C) { + testServer.Response(400, nil, ErrorDump) + + options := ec2.RunInstances{ + ImageId: "ami-a6f504cf", // Ubuntu Maverick, i386, instance store + InstanceType: "t1.micro", // Doesn't work with micro, results in 400. + } + + msg := `AMIs with an instance-store root device are not supported for the instance type 't1\.micro'\.` + + resp, err := s.ec2.RunInstances(&options) + + testServer.WaitRequest() + + c.Assert(resp, IsNil) + c.Assert(err, ErrorMatches, msg+` \(UnsupportedOperation\)`) + + ec2err, ok := err.(*ec2.Error) + c.Assert(ok, Equals, true) + c.Assert(ec2err.StatusCode, Equals, 400) + c.Assert(ec2err.Code, Equals, "UnsupportedOperation") + c.Assert(ec2err.Message, Matches, msg) + c.Assert(ec2err.RequestId, Equals, "0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4") +} + +func (s *S) TestRequestSpotInstancesErrorDump(c *C) { + testServer.Response(400, nil, ErrorDump) + + options := ec2.RequestSpotInstances{ + SpotPrice: "0.01", + ImageId: "ami-a6f504cf", // Ubuntu Maverick, i386, instance store + InstanceType: "t1.micro", // Doesn't work with micro, results in 400. + } + + msg := `AMIs with an instance-store root device are not supported for the instance type 't1\.micro'\.` + + resp, err := s.ec2.RequestSpotInstances(&options) + + testServer.WaitRequest() + + c.Assert(resp, IsNil) + c.Assert(err, ErrorMatches, msg+` \(UnsupportedOperation\)`) + + ec2err, ok := err.(*ec2.Error) + c.Assert(ok, Equals, true) + c.Assert(ec2err.StatusCode, Equals, 400) + c.Assert(ec2err.Code, Equals, "UnsupportedOperation") + c.Assert(ec2err.Message, Matches, msg) + c.Assert(ec2err.RequestId, Equals, "0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4") +} + +func (s *S) TestRunInstancesErrorWithoutXML(c *C) { + testServer.Responses(5, 500, nil, "") + options := ec2.RunInstances{ImageId: "image-id"} + + resp, err := s.ec2.RunInstances(&options) + + testServer.WaitRequest() + + c.Assert(resp, IsNil) + c.Assert(err, ErrorMatches, "") + + ec2err, ok := err.(*ec2.Error) + c.Assert(ok, Equals, true) + c.Assert(ec2err.StatusCode, Equals, 500) + c.Assert(ec2err.Code, Equals, "") + c.Assert(ec2err.Message, Equals, "") + c.Assert(ec2err.RequestId, Equals, "") +} + +func (s *S) TestRequestSpotInstancesErrorWithoutXML(c *C) { + testServer.Responses(5, 500, nil, "") + options := ec2.RequestSpotInstances{SpotPrice: "spot-price", ImageId: "image-id"} + + resp, err := s.ec2.RequestSpotInstances(&options) + + testServer.WaitRequest() + + c.Assert(resp, IsNil) + c.Assert(err, ErrorMatches, "") + + ec2err, ok := err.(*ec2.Error) + c.Assert(ok, Equals, true) + c.Assert(ec2err.StatusCode, Equals, 500) + c.Assert(ec2err.Code, Equals, "") + c.Assert(ec2err.Message, Equals, "") + c.Assert(ec2err.RequestId, Equals, "") +} + +func (s *S) TestRunInstancesExample(c *C) { + testServer.Response(200, nil, RunInstancesExample) + + options := ec2.RunInstances{ + KeyName: "my-keys", + ImageId: "image-id", + InstanceType: "inst-type", + SecurityGroups: []ec2.SecurityGroup{{Name: "g1"}, {Id: "g2"}, {Name: "g3"}, {Id: "g4"}}, + UserData: []byte("1234"), + KernelId: "kernel-id", + RamdiskId: "ramdisk-id", + AvailZone: "zone", + Tenancy: "dedicated", + PlacementGroupName: "group", + Monitoring: true, + SubnetId: "subnet-id", + DisableAPITermination: true, + EbsOptimized: true, + ShutdownBehavior: "terminate", + PrivateIPAddress: "10.0.0.25", + BlockDevices: []ec2.BlockDeviceMapping{ + {DeviceName: "/dev/sdb", VirtualName: "ephemeral0"}, + {DeviceName: "/dev/sdc", SnapshotId: "snap-a08912c9", DeleteOnTermination: true}, + }, + } + resp, err := s.ec2.RunInstances(&options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"RunInstances"}) + c.Assert(req.Form["ImageId"], DeepEquals, []string{"image-id"}) + c.Assert(req.Form["MinCount"], DeepEquals, []string{"1"}) + c.Assert(req.Form["MaxCount"], DeepEquals, []string{"1"}) + c.Assert(req.Form["KeyName"], DeepEquals, []string{"my-keys"}) + c.Assert(req.Form["InstanceType"], DeepEquals, []string{"inst-type"}) + c.Assert(req.Form["SecurityGroup.1"], DeepEquals, []string{"g1"}) + c.Assert(req.Form["SecurityGroup.2"], DeepEquals, []string{"g3"}) + c.Assert(req.Form["SecurityGroupId.1"], DeepEquals, []string{"g2"}) + c.Assert(req.Form["SecurityGroupId.2"], DeepEquals, []string{"g4"}) + c.Assert(req.Form["UserData"], DeepEquals, []string{"MTIzNA=="}) + c.Assert(req.Form["KernelId"], DeepEquals, []string{"kernel-id"}) + c.Assert(req.Form["RamdiskId"], DeepEquals, []string{"ramdisk-id"}) + c.Assert(req.Form["Placement.AvailabilityZone"], DeepEquals, []string{"zone"}) + c.Assert(req.Form["Placement.GroupName"], DeepEquals, []string{"group"}) + c.Assert(req.Form["Monitoring.Enabled"], DeepEquals, []string{"true"}) + c.Assert(req.Form["SubnetId"], DeepEquals, []string{"subnet-id"}) + c.Assert(req.Form["DisableApiTermination"], DeepEquals, []string{"true"}) + c.Assert(req.Form["EbsOptimized"], DeepEquals, []string{"true"}) + c.Assert(req.Form["InstanceInitiatedShutdownBehavior"], DeepEquals, []string{"terminate"}) + c.Assert(req.Form["PrivateIpAddress"], DeepEquals, []string{"10.0.0.25"}) + c.Assert(req.Form["BlockDeviceMapping.1.DeviceName"], DeepEquals, []string{"/dev/sdb"}) + c.Assert(req.Form["BlockDeviceMapping.1.VirtualName"], DeepEquals, []string{"ephemeral0"}) + c.Assert(req.Form["BlockDeviceMapping.2.Ebs.SnapshotId"], DeepEquals, []string{"snap-a08912c9"}) + c.Assert(req.Form["BlockDeviceMapping.2.Ebs.DeleteOnTermination"], DeepEquals, []string{"true"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.ReservationId, Equals, "r-47a5402e") + c.Assert(resp.OwnerId, Equals, "999988887777") + c.Assert(resp.SecurityGroups, DeepEquals, []ec2.SecurityGroup{{Name: "default", Id: "sg-67ad940e"}}) + c.Assert(resp.Instances, HasLen, 3) + + i0 := resp.Instances[0] + c.Assert(i0.InstanceId, Equals, "i-2ba64342") + c.Assert(i0.InstanceType, Equals, "m1.small") + c.Assert(i0.ImageId, Equals, "ami-60a54009") + c.Assert(i0.Monitoring, Equals, "enabled") + c.Assert(i0.KeyName, Equals, "example-key-name") + c.Assert(i0.AMILaunchIndex, Equals, 0) + c.Assert(i0.VirtType, Equals, "paravirtual") + c.Assert(i0.Hypervisor, Equals, "xen") + + i1 := resp.Instances[1] + c.Assert(i1.InstanceId, Equals, "i-2bc64242") + c.Assert(i1.InstanceType, Equals, "m1.small") + c.Assert(i1.ImageId, Equals, "ami-60a54009") + c.Assert(i1.Monitoring, Equals, "enabled") + c.Assert(i1.KeyName, Equals, "example-key-name") + c.Assert(i1.AMILaunchIndex, Equals, 1) + c.Assert(i1.VirtType, Equals, "paravirtual") + c.Assert(i1.Hypervisor, Equals, "xen") + + i2 := resp.Instances[2] + c.Assert(i2.InstanceId, Equals, "i-2be64332") + c.Assert(i2.InstanceType, Equals, "m1.small") + c.Assert(i2.ImageId, Equals, "ami-60a54009") + c.Assert(i2.Monitoring, Equals, "enabled") + c.Assert(i2.KeyName, Equals, "example-key-name") + c.Assert(i2.AMILaunchIndex, Equals, 2) + c.Assert(i2.VirtType, Equals, "paravirtual") + c.Assert(i2.Hypervisor, Equals, "xen") +} + +func (s *S) TestRequestSpotInstancesExample(c *C) { + testServer.Response(200, nil, RequestSpotInstancesExample) + + options := ec2.RequestSpotInstances{ + SpotPrice: "0.5", + KeyName: "my-keys", + ImageId: "image-id", + InstanceType: "inst-type", + SecurityGroups: []ec2.SecurityGroup{{Name: "g1"}, {Id: "g2"}, {Name: "g3"}, {Id: "g4"}}, + UserData: []byte("1234"), + KernelId: "kernel-id", + RamdiskId: "ramdisk-id", + AvailZone: "zone", + PlacementGroupName: "group", + Monitoring: true, + SubnetId: "subnet-id", + PrivateIPAddress: "10.0.0.25", + BlockDevices: []ec2.BlockDeviceMapping{ + {DeviceName: "/dev/sdb", VirtualName: "ephemeral0"}, + {DeviceName: "/dev/sdc", SnapshotId: "snap-a08912c9", DeleteOnTermination: true}, + }, + } + resp, err := s.ec2.RequestSpotInstances(&options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"RequestSpotInstances"}) + c.Assert(req.Form["SpotPrice"], DeepEquals, []string{"0.5"}) + c.Assert(req.Form["LaunchSpecification.ImageId"], DeepEquals, []string{"image-id"}) + c.Assert(req.Form["LaunchSpecification.KeyName"], DeepEquals, []string{"my-keys"}) + c.Assert(req.Form["LaunchSpecification.InstanceType"], DeepEquals, []string{"inst-type"}) + c.Assert(req.Form["LaunchSpecification.SecurityGroup.1"], DeepEquals, []string{"g1"}) + c.Assert(req.Form["LaunchSpecification.SecurityGroup.2"], DeepEquals, []string{"g3"}) + c.Assert(req.Form["LaunchSpecification.SecurityGroupId.1"], DeepEquals, []string{"g2"}) + c.Assert(req.Form["LaunchSpecification.SecurityGroupId.2"], DeepEquals, []string{"g4"}) + c.Assert(req.Form["LaunchSpecification.UserData"], DeepEquals, []string{"MTIzNA=="}) + c.Assert(req.Form["LaunchSpecification.KernelId"], DeepEquals, []string{"kernel-id"}) + c.Assert(req.Form["LaunchSpecification.RamdiskId"], DeepEquals, []string{"ramdisk-id"}) + c.Assert(req.Form["LaunchSpecification.Placement.AvailabilityZone"], DeepEquals, []string{"zone"}) + c.Assert(req.Form["LaunchSpecification.Placement.GroupName"], DeepEquals, []string{"group"}) + c.Assert(req.Form["LaunchSpecification.Monitoring.Enabled"], DeepEquals, []string{"true"}) + c.Assert(req.Form["LaunchSpecification.SubnetId"], DeepEquals, []string{"subnet-id"}) + c.Assert(req.Form["LaunchSpecification.PrivateIpAddress"], DeepEquals, []string{"10.0.0.25"}) + c.Assert(req.Form["LaunchSpecification.BlockDeviceMapping.1.DeviceName"], DeepEquals, []string{"/dev/sdb"}) + c.Assert(req.Form["LaunchSpecification.BlockDeviceMapping.1.VirtualName"], DeepEquals, []string{"ephemeral0"}) + c.Assert(req.Form["LaunchSpecification.BlockDeviceMapping.2.Ebs.SnapshotId"], DeepEquals, []string{"snap-a08912c9"}) + c.Assert(req.Form["LaunchSpecification.BlockDeviceMapping.2.Ebs.DeleteOnTermination"], DeepEquals, []string{"true"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.SpotRequestResults[0].SpotRequestId, Equals, "sir-1a2b3c4d") + c.Assert(resp.SpotRequestResults[0].SpotPrice, Equals, "0.5") + c.Assert(resp.SpotRequestResults[0].State, Equals, "open") + c.Assert(resp.SpotRequestResults[0].SpotLaunchSpec.ImageId, Equals, "ami-1a2b3c4d") + c.Assert(resp.SpotRequestResults[0].Status.Code, Equals, "pending-evaluation") + c.Assert(resp.SpotRequestResults[0].Status.UpdateTime, Equals, "2008-05-07T12:51:50.000Z") + c.Assert(resp.SpotRequestResults[0].Status.Message, Equals, "Your Spot request has been submitted for review, and is pending evaluation.") +} + +func (s *S) TestCancelSpotRequestsExample(c *C) { + testServer.Response(200, nil, CancelSpotRequestsExample) + + resp, err := s.ec2.CancelSpotRequests([]string{"s-1", "s-2"}) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CancelSpotInstanceRequests"}) + c.Assert(req.Form["SpotInstanceRequestId.1"], DeepEquals, []string{"s-1"}) + c.Assert(req.Form["SpotInstanceRequestId.2"], DeepEquals, []string{"s-2"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.CancelSpotRequestResults[0].SpotRequestId, Equals, "sir-1a2b3c4d") + c.Assert(resp.CancelSpotRequestResults[0].State, Equals, "cancelled") +} + +func (s *S) TestTerminateInstancesExample(c *C) { + testServer.Response(200, nil, TerminateInstancesExample) + + resp, err := s.ec2.TerminateInstances([]string{"i-1", "i-2"}) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"TerminateInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-1"}) + c.Assert(req.Form["InstanceId.2"], DeepEquals, []string{"i-2"}) + c.Assert(req.Form["UserData"], IsNil) + c.Assert(req.Form["KernelId"], IsNil) + c.Assert(req.Form["RamdiskId"], IsNil) + c.Assert(req.Form["Placement.AvailabilityZone"], IsNil) + c.Assert(req.Form["Placement.GroupName"], IsNil) + c.Assert(req.Form["Monitoring.Enabled"], IsNil) + c.Assert(req.Form["SubnetId"], IsNil) + c.Assert(req.Form["DisableApiTermination"], IsNil) + c.Assert(req.Form["EbsOptimized"], IsNil) + c.Assert(req.Form["InstanceInitiatedShutdownBehavior"], IsNil) + c.Assert(req.Form["PrivateIpAddress"], IsNil) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.StateChanges, HasLen, 1) + c.Assert(resp.StateChanges[0].InstanceId, Equals, "i-3ea74257") + c.Assert(resp.StateChanges[0].CurrentState.Code, Equals, 32) + c.Assert(resp.StateChanges[0].CurrentState.Name, Equals, "shutting-down") + c.Assert(resp.StateChanges[0].PreviousState.Code, Equals, 16) + c.Assert(resp.StateChanges[0].PreviousState.Name, Equals, "running") +} + +func (s *S) TestDescribeSpotRequestsExample(c *C) { + testServer.Response(200, nil, DescribeSpotRequestsExample) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.DescribeSpotRequests([]string{"s-1", "s-2"}, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSpotInstanceRequests"}) + c.Assert(req.Form["SpotInstanceRequestId.1"], DeepEquals, []string{"s-1"}) + c.Assert(req.Form["SpotInstanceRequestId.2"], DeepEquals, []string{"s-2"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "b1719f2a-5334-4479-b2f1-26926EXAMPLE") + c.Assert(resp.SpotRequestResults[0].SpotRequestId, Equals, "sir-1a2b3c4d") + c.Assert(resp.SpotRequestResults[0].State, Equals, "active") + c.Assert(resp.SpotRequestResults[0].SpotPrice, Equals, "0.5") + c.Assert(resp.SpotRequestResults[0].SpotLaunchSpec.ImageId, Equals, "ami-1a2b3c4d") + c.Assert(resp.SpotRequestResults[0].Status.Code, Equals, "fulfilled") + c.Assert(resp.SpotRequestResults[0].Status.UpdateTime, Equals, "2008-05-07T12:51:50.000Z") + c.Assert(resp.SpotRequestResults[0].Status.Message, Equals, "Your Spot request is fulfilled.") +} + +func (s *S) TestDescribeInstancesExample1(c *C) { + testServer.Response(200, nil, DescribeInstancesExample1) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.Instances([]string{"i-1", "i-2"}, nil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-1"}) + c.Assert(req.Form["InstanceId.2"], DeepEquals, []string{"i-2"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "98e3c9a4-848c-4d6d-8e8a-b1bdEXAMPLE") + c.Assert(resp.Reservations, HasLen, 2) + + r0 := resp.Reservations[0] + c.Assert(r0.ReservationId, Equals, "r-b27e30d9") + c.Assert(r0.OwnerId, Equals, "999988887777") + c.Assert(r0.RequesterId, Equals, "854251627541") + c.Assert(r0.SecurityGroups, DeepEquals, []ec2.SecurityGroup{{Name: "default", Id: "sg-67ad940e"}}) + c.Assert(r0.Instances, HasLen, 1) + + r0i := r0.Instances[0] + c.Assert(r0i.InstanceId, Equals, "i-c5cd56af") + c.Assert(r0i.PrivateDNSName, Equals, "domU-12-31-39-10-56-34.compute-1.internal") + c.Assert(r0i.DNSName, Equals, "ec2-174-129-165-232.compute-1.amazonaws.com") + c.Assert(r0i.AvailZone, Equals, "us-east-1b") + + b0 := r0i.BlockDevices[0] + c.Assert(b0.DeviceName, Equals, "/dev/sda1") + c.Assert(b0.VolumeId, Equals, "vol-a082c1c9") + c.Assert(b0.Status, Equals, "attached") + c.Assert(b0.AttachTime, Equals, "2010-08-17T01:15:21.000Z") + c.Assert(b0.DeleteOnTermination, Equals, false) +} + +func (s *S) TestDescribeInstancesExample2(c *C) { + testServer.Response(200, nil, DescribeInstancesExample2) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.Instances([]string{"i-1", "i-2"}, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-1"}) + c.Assert(req.Form["InstanceId.2"], DeepEquals, []string{"i-2"}) + c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"}) + c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"}) + c.Assert(req.Form["Filter.1.Value.2"], IsNil) + c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"}) + c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"}) + c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Reservations, HasLen, 1) + + r0 := resp.Reservations[0] + r0i := r0.Instances[0] + c.Assert(r0i.State.Code, Equals, 16) + c.Assert(r0i.State.Name, Equals, "running") + + r0t0 := r0i.Tags[0] + r0t1 := r0i.Tags[1] + c.Assert(r0t0.Key, Equals, "webserver") + c.Assert(r0t0.Value, Equals, "") + c.Assert(r0t1.Key, Equals, "stack") + c.Assert(r0t1.Value, Equals, "Production") +} + +func (s *S) TestCreateImageExample(c *C) { + testServer.Response(200, nil, CreateImageExample) + + options := &ec2.CreateImage{ + InstanceId: "i-123456", + Name: "foo", + Description: "Test CreateImage", + NoReboot: true, + BlockDevices: []ec2.BlockDeviceMapping{ + {DeviceName: "/dev/sdb", VirtualName: "ephemeral0"}, + {DeviceName: "/dev/sdc", SnapshotId: "snap-a08912c9", DeleteOnTermination: true}, + }, + } + + resp, err := s.ec2.CreateImage(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CreateImage"}) + c.Assert(req.Form["InstanceId"], DeepEquals, []string{options.InstanceId}) + c.Assert(req.Form["Name"], DeepEquals, []string{options.Name}) + c.Assert(req.Form["Description"], DeepEquals, []string{options.Description}) + c.Assert(req.Form["NoReboot"], DeepEquals, []string{"true"}) + c.Assert(req.Form["BlockDeviceMapping.1.DeviceName"], DeepEquals, []string{"/dev/sdb"}) + c.Assert(req.Form["BlockDeviceMapping.1.VirtualName"], DeepEquals, []string{"ephemeral0"}) + c.Assert(req.Form["BlockDeviceMapping.2.DeviceName"], DeepEquals, []string{"/dev/sdc"}) + c.Assert(req.Form["BlockDeviceMapping.2.Ebs.SnapshotId"], DeepEquals, []string{"snap-a08912c9"}) + c.Assert(req.Form["BlockDeviceMapping.2.Ebs.DeleteOnTermination"], DeepEquals, []string{"true"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.ImageId, Equals, "ami-4fa54026") +} + +func (s *S) TestDescribeImagesExample(c *C) { + testServer.Response(200, nil, DescribeImagesExample) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.Images([]string{"ami-1", "ami-2"}, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeImages"}) + c.Assert(req.Form["ImageId.1"], DeepEquals, []string{"ami-1"}) + c.Assert(req.Form["ImageId.2"], DeepEquals, []string{"ami-2"}) + c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"}) + c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"}) + c.Assert(req.Form["Filter.1.Value.2"], IsNil) + c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"}) + c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"}) + c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "4a4a27a2-2e7c-475d-b35b-ca822EXAMPLE") + c.Assert(resp.Images, HasLen, 1) + + i0 := resp.Images[0] + c.Assert(i0.Id, Equals, "ami-a2469acf") + c.Assert(i0.Type, Equals, "machine") + c.Assert(i0.Name, Equals, "example-marketplace-amzn-ami.1") + c.Assert(i0.Description, Equals, "Amazon Linux AMI i386 EBS") + c.Assert(i0.Location, Equals, "aws-marketplace/example-marketplace-amzn-ami.1") + c.Assert(i0.State, Equals, "available") + c.Assert(i0.Public, Equals, true) + c.Assert(i0.OwnerId, Equals, "123456789999") + c.Assert(i0.OwnerAlias, Equals, "aws-marketplace") + c.Assert(i0.Architecture, Equals, "i386") + c.Assert(i0.KernelId, Equals, "aki-805ea7e9") + c.Assert(i0.RootDeviceType, Equals, "ebs") + c.Assert(i0.RootDeviceName, Equals, "/dev/sda1") + c.Assert(i0.VirtualizationType, Equals, "paravirtual") + c.Assert(i0.Hypervisor, Equals, "xen") + + c.Assert(i0.BlockDevices, HasLen, 1) + c.Assert(i0.BlockDevices[0].DeviceName, Equals, "/dev/sda1") + c.Assert(i0.BlockDevices[0].SnapshotId, Equals, "snap-787e9403") + c.Assert(i0.BlockDevices[0].VolumeSize, Equals, int64(8)) + c.Assert(i0.BlockDevices[0].DeleteOnTermination, Equals, true) + + testServer.Response(200, nil, DescribeImagesExample) + resp2, err := s.ec2.ImagesByOwners([]string{"ami-1", "ami-2"}, []string{"123456789999", "id2"}, filter) + + req2 := testServer.WaitRequest() + c.Assert(req2.Form["Action"], DeepEquals, []string{"DescribeImages"}) + c.Assert(req2.Form["ImageId.1"], DeepEquals, []string{"ami-1"}) + c.Assert(req2.Form["ImageId.2"], DeepEquals, []string{"ami-2"}) + c.Assert(req2.Form["Owner.1"], DeepEquals, []string{"123456789999"}) + c.Assert(req2.Form["Owner.2"], DeepEquals, []string{"id2"}) + c.Assert(req2.Form["Filter.1.Name"], DeepEquals, []string{"key1"}) + c.Assert(req2.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"}) + c.Assert(req2.Form["Filter.1.Value.2"], IsNil) + c.Assert(req2.Form["Filter.2.Name"], DeepEquals, []string{"key2"}) + c.Assert(req2.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"}) + c.Assert(req2.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"}) + + c.Assert(err, IsNil) + c.Assert(resp2.RequestId, Equals, "4a4a27a2-2e7c-475d-b35b-ca822EXAMPLE") + c.Assert(resp2.Images, HasLen, 1) + + i1 := resp2.Images[0] + c.Assert(i1.Id, Equals, "ami-a2469acf") + c.Assert(i1.Type, Equals, "machine") + c.Assert(i1.Name, Equals, "example-marketplace-amzn-ami.1") + c.Assert(i1.Description, Equals, "Amazon Linux AMI i386 EBS") + c.Assert(i1.Location, Equals, "aws-marketplace/example-marketplace-amzn-ami.1") + c.Assert(i1.State, Equals, "available") + c.Assert(i1.Public, Equals, true) + c.Assert(i1.OwnerId, Equals, "123456789999") + c.Assert(i1.OwnerAlias, Equals, "aws-marketplace") + c.Assert(i1.Architecture, Equals, "i386") + c.Assert(i1.KernelId, Equals, "aki-805ea7e9") + c.Assert(i1.RootDeviceType, Equals, "ebs") + c.Assert(i1.RootDeviceName, Equals, "/dev/sda1") + c.Assert(i1.VirtualizationType, Equals, "paravirtual") + c.Assert(i1.Hypervisor, Equals, "xen") + + c.Assert(i1.BlockDevices, HasLen, 1) + c.Assert(i1.BlockDevices[0].DeviceName, Equals, "/dev/sda1") + c.Assert(i1.BlockDevices[0].SnapshotId, Equals, "snap-787e9403") + c.Assert(i1.BlockDevices[0].VolumeSize, Equals, int64(8)) + c.Assert(i1.BlockDevices[0].DeleteOnTermination, Equals, true) +} + +func (s *S) TestImageAttributeExample(c *C) { + testServer.Response(200, nil, ImageAttributeExample) + + resp, err := s.ec2.ImageAttribute("ami-61a54008", "launchPermission") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeImageAttribute"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.ImageId, Equals, "ami-61a54008") + c.Assert(resp.Group, Equals, "all") + c.Assert(resp.UserIds[0], Equals, "495219933132") +} + +func (s *S) TestCreateSnapshotExample(c *C) { + testServer.Response(200, nil, CreateSnapshotExample) + + resp, err := s.ec2.CreateSnapshot("vol-4d826724", "Daily Backup") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CreateSnapshot"}) + c.Assert(req.Form["VolumeId"], DeepEquals, []string{"vol-4d826724"}) + c.Assert(req.Form["Description"], DeepEquals, []string{"Daily Backup"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Snapshot.Id, Equals, "snap-78a54011") + c.Assert(resp.Snapshot.VolumeId, Equals, "vol-4d826724") + c.Assert(resp.Snapshot.Status, Equals, "pending") + c.Assert(resp.Snapshot.StartTime, Equals, "2008-05-07T12:51:50.000Z") + c.Assert(resp.Snapshot.Progress, Equals, "60%") + c.Assert(resp.Snapshot.OwnerId, Equals, "111122223333") + c.Assert(resp.Snapshot.VolumeSize, Equals, "10") + c.Assert(resp.Snapshot.Description, Equals, "Daily Backup") +} + +func (s *S) TestDeleteSnapshotsExample(c *C) { + testServer.Response(200, nil, DeleteSnapshotExample) + + resp, err := s.ec2.DeleteSnapshots([]string{"snap-78a54011"}) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteSnapshot"}) + c.Assert(req.Form["SnapshotId.1"], DeepEquals, []string{"snap-78a54011"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestDescribeSnapshotsExample(c *C) { + testServer.Response(200, nil, DescribeSnapshotsExample) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.Snapshots([]string{"snap-1", "snap-2"}, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSnapshots"}) + c.Assert(req.Form["SnapshotId.1"], DeepEquals, []string{"snap-1"}) + c.Assert(req.Form["SnapshotId.2"], DeepEquals, []string{"snap-2"}) + c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"}) + c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"}) + c.Assert(req.Form["Filter.1.Value.2"], IsNil) + c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"}) + c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"}) + c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Snapshots, HasLen, 1) + + s0 := resp.Snapshots[0] + c.Assert(s0.Id, Equals, "snap-1a2b3c4d") + c.Assert(s0.VolumeId, Equals, "vol-8875daef") + c.Assert(s0.VolumeSize, Equals, "15") + c.Assert(s0.Status, Equals, "pending") + c.Assert(s0.StartTime, Equals, "2010-07-29T04:12:01.000Z") + c.Assert(s0.Progress, Equals, "30%") + c.Assert(s0.OwnerId, Equals, "111122223333") + c.Assert(s0.Description, Equals, "Daily Backup") + + c.Assert(s0.Tags, HasLen, 1) + c.Assert(s0.Tags[0].Key, Equals, "Purpose") + c.Assert(s0.Tags[0].Value, Equals, "demo_db_14_backup") +} + +func (s *S) TestModifyImageAttributeExample(c *C) { + testServer.Response(200, nil, ModifyImageAttributeExample) + + options := ec2.ModifyImageAttribute{ + Description: "Test Description", + } + + resp, err := s.ec2.ModifyImageAttribute("ami-4fa54026", &options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"ModifyImageAttribute"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestModifyImageAttributeExample_complex(c *C) { + testServer.Response(200, nil, ModifyImageAttributeExample) + + options := ec2.ModifyImageAttribute{ + AddUsers: []string{"u1", "u2"}, + RemoveUsers: []string{"u3"}, + AddGroups: []string{"g1", "g3"}, + RemoveGroups: []string{"g2"}, + Description: "Test Description", + } + + resp, err := s.ec2.ModifyImageAttribute("ami-4fa54026", &options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"ModifyImageAttribute"}) + c.Assert(req.Form["LaunchPermission.Add.1.UserId"], DeepEquals, []string{"u1"}) + c.Assert(req.Form["LaunchPermission.Add.2.UserId"], DeepEquals, []string{"u2"}) + c.Assert(req.Form["LaunchPermission.Remove.1.UserId"], DeepEquals, []string{"u3"}) + c.Assert(req.Form["LaunchPermission.Add.1.Group"], DeepEquals, []string{"g1"}) + c.Assert(req.Form["LaunchPermission.Add.2.Group"], DeepEquals, []string{"g3"}) + c.Assert(req.Form["LaunchPermission.Remove.1.Group"], DeepEquals, []string{"g2"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestCopyImageExample(c *C) { + testServer.Response(200, nil, CopyImageExample) + + options := ec2.CopyImage{ + SourceRegion: "us-west-2", + SourceImageId: "ami-1a2b3c4d", + Description: "Test Description", + } + + resp, err := s.ec2.CopyImage(&options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CopyImage"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "60bc441d-fa2c-494d-b155-5d6a3EXAMPLE") +} + +func (s *S) TestCreateKeyPairExample(c *C) { + testServer.Response(200, nil, CreateKeyPairExample) + + resp, err := s.ec2.CreateKeyPair("foo") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CreateKeyPair"}) + c.Assert(req.Form["KeyName"], DeepEquals, []string{"foo"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.KeyName, Equals, "foo") + c.Assert(resp.KeyFingerprint, Equals, "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00") +} + +func (s *S) TestDeleteKeyPairExample(c *C) { + testServer.Response(200, nil, DeleteKeyPairExample) + + resp, err := s.ec2.DeleteKeyPair("foo") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteKeyPair"}) + c.Assert(req.Form["KeyName"], DeepEquals, []string{"foo"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestCreateSecurityGroupExample(c *C) { + testServer.Response(200, nil, CreateSecurityGroupExample) + + resp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: "websrv", Description: "Web Servers"}) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CreateSecurityGroup"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(req.Form["GroupDescription"], DeepEquals, []string{"Web Servers"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Name, Equals, "websrv") + c.Assert(resp.Id, Equals, "sg-67ad940e") +} + +func (s *S) TestDescribeSecurityGroupsExample(c *C) { + testServer.Response(200, nil, DescribeSecurityGroupsExample) + + resp, err := s.ec2.SecurityGroups([]ec2.SecurityGroup{{Name: "WebServers"}, {Name: "RangedPortsBySource"}}, nil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSecurityGroups"}) + c.Assert(req.Form["GroupName.1"], DeepEquals, []string{"WebServers"}) + c.Assert(req.Form["GroupName.2"], DeepEquals, []string{"RangedPortsBySource"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Groups, HasLen, 2) + + g0 := resp.Groups[0] + c.Assert(g0.OwnerId, Equals, "999988887777") + c.Assert(g0.Name, Equals, "WebServers") + c.Assert(g0.Id, Equals, "sg-67ad940e") + c.Assert(g0.Description, Equals, "Web Servers") + c.Assert(g0.IPPerms, HasLen, 1) + + g0ipp := g0.IPPerms[0] + c.Assert(g0ipp.Protocol, Equals, "tcp") + c.Assert(g0ipp.FromPort, Equals, 80) + c.Assert(g0ipp.ToPort, Equals, 80) + c.Assert(g0ipp.SourceIPs, DeepEquals, []string{"0.0.0.0/0"}) + + g1 := resp.Groups[1] + c.Assert(g1.OwnerId, Equals, "999988887777") + c.Assert(g1.Name, Equals, "RangedPortsBySource") + c.Assert(g1.Id, Equals, "sg-76abc467") + c.Assert(g1.Description, Equals, "Group A") + c.Assert(g1.IPPerms, HasLen, 1) + + g1ipp := g1.IPPerms[0] + c.Assert(g1ipp.Protocol, Equals, "tcp") + c.Assert(g1ipp.FromPort, Equals, 6000) + c.Assert(g1ipp.ToPort, Equals, 7000) + c.Assert(g1ipp.SourceIPs, IsNil) +} + +func (s *S) TestDescribeSecurityGroupsExampleWithFilter(c *C) { + testServer.Response(200, nil, DescribeSecurityGroupsExample) + + filter := ec2.NewFilter() + filter.Add("ip-permission.protocol", "tcp") + filter.Add("ip-permission.from-port", "22") + filter.Add("ip-permission.to-port", "22") + filter.Add("ip-permission.group-name", "app_server_group", "database_group") + + _, err := s.ec2.SecurityGroups(nil, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSecurityGroups"}) + c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"ip-permission.from-port"}) + c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"22"}) + c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"ip-permission.group-name"}) + c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"app_server_group"}) + c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"database_group"}) + c.Assert(req.Form["Filter.3.Name"], DeepEquals, []string{"ip-permission.protocol"}) + c.Assert(req.Form["Filter.3.Value.1"], DeepEquals, []string{"tcp"}) + c.Assert(req.Form["Filter.4.Name"], DeepEquals, []string{"ip-permission.to-port"}) + c.Assert(req.Form["Filter.4.Value.1"], DeepEquals, []string{"22"}) + + c.Assert(err, IsNil) +} + +func (s *S) TestDescribeSecurityGroupsDumpWithGroup(c *C) { + testServer.Response(200, nil, DescribeSecurityGroupsDump) + + resp, err := s.ec2.SecurityGroups(nil, nil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSecurityGroups"}) + c.Assert(err, IsNil) + c.Check(resp.Groups, HasLen, 1) + c.Check(resp.Groups[0].IPPerms, HasLen, 2) + + ipp0 := resp.Groups[0].IPPerms[0] + c.Assert(ipp0.SourceIPs, IsNil) + c.Check(ipp0.Protocol, Equals, "icmp") + c.Assert(ipp0.SourceGroups, HasLen, 1) + c.Check(ipp0.SourceGroups[0].OwnerId, Equals, "12345") + c.Check(ipp0.SourceGroups[0].Name, Equals, "default") + c.Check(ipp0.SourceGroups[0].Id, Equals, "sg-67ad940e") + + ipp1 := resp.Groups[0].IPPerms[1] + c.Check(ipp1.Protocol, Equals, "tcp") + c.Assert(ipp0.SourceIPs, IsNil) + c.Assert(ipp0.SourceGroups, HasLen, 1) + c.Check(ipp1.SourceGroups[0].Id, Equals, "sg-76abc467") + c.Check(ipp1.SourceGroups[0].OwnerId, Equals, "12345") + c.Check(ipp1.SourceGroups[0].Name, Equals, "other") +} + +func (s *S) TestDeleteSecurityGroupExample(c *C) { + testServer.Response(200, nil, DeleteSecurityGroupExample) + + resp, err := s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: "websrv"}) + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteSecurityGroup"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(req.Form["GroupId"], IsNil) + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestDeleteSecurityGroupExampleWithId(c *C) { + testServer.Response(200, nil, DeleteSecurityGroupExample) + + // ignore return and error - we're only want to check the parameter handling. + s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Id: "sg-67ad940e", Name: "ignored"}) + req := testServer.WaitRequest() + + c.Assert(req.Form["GroupName"], IsNil) + c.Assert(req.Form["GroupId"], DeepEquals, []string{"sg-67ad940e"}) +} + +func (s *S) TestAuthorizeSecurityGroupExample1(c *C) { + testServer.Response(200, nil, AuthorizeSecurityGroupIngressExample) + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 80, + ToPort: 80, + SourceIPs: []string{"205.192.0.0/16", "205.159.0.0/16"}, + }} + resp, err := s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: "websrv"}, perms) + + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"AuthorizeSecurityGroupIngress"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(req.Form["IpPermissions.1.IpProtocol"], DeepEquals, []string{"tcp"}) + c.Assert(req.Form["IpPermissions.1.FromPort"], DeepEquals, []string{"80"}) + c.Assert(req.Form["IpPermissions.1.ToPort"], DeepEquals, []string{"80"}) + c.Assert(req.Form["IpPermissions.1.IpRanges.1.CidrIp"], DeepEquals, []string{"205.192.0.0/16"}) + c.Assert(req.Form["IpPermissions.1.IpRanges.2.CidrIp"], DeepEquals, []string{"205.159.0.0/16"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestAuthorizeSecurityGroupEgress(c *C) { + testServer.Response(200, nil, AuthorizeSecurityGroupEgressExample) + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 80, + ToPort: 80, + SourceIPs: []string{"205.192.0.0/16", "205.159.0.0/16"}, + }} + resp, err := s.ec2.AuthorizeSecurityGroupEgress(ec2.SecurityGroup{Name: "websrv"}, perms) + + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"AuthorizeSecurityGroupEgress"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(req.Form["IpPermissions.1.IpProtocol"], DeepEquals, []string{"tcp"}) + c.Assert(req.Form["IpPermissions.1.FromPort"], DeepEquals, []string{"80"}) + c.Assert(req.Form["IpPermissions.1.ToPort"], DeepEquals, []string{"80"}) + c.Assert(req.Form["IpPermissions.1.IpRanges.1.CidrIp"], DeepEquals, []string{"205.192.0.0/16"}) + c.Assert(req.Form["IpPermissions.1.IpRanges.2.CidrIp"], DeepEquals, []string{"205.159.0.0/16"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestAuthorizeSecurityGroupExample1WithId(c *C) { + testServer.Response(200, nil, AuthorizeSecurityGroupIngressExample) + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 80, + ToPort: 80, + SourceIPs: []string{"205.192.0.0/16", "205.159.0.0/16"}, + }} + // ignore return and error - we're only want to check the parameter handling. + s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Id: "sg-67ad940e", Name: "ignored"}, perms) + + req := testServer.WaitRequest() + + c.Assert(req.Form["GroupName"], IsNil) + c.Assert(req.Form["GroupId"], DeepEquals, []string{"sg-67ad940e"}) +} + +func (s *S) TestAuthorizeSecurityGroupExample2(c *C) { + testServer.Response(200, nil, AuthorizeSecurityGroupIngressExample) + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 80, + ToPort: 81, + SourceGroups: []ec2.UserSecurityGroup{ + {OwnerId: "999988887777", Name: "OtherAccountGroup"}, + {Id: "sg-67ad940e"}, + }, + }} + resp, err := s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: "websrv"}, perms) + + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"AuthorizeSecurityGroupIngress"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(req.Form["IpPermissions.1.IpProtocol"], DeepEquals, []string{"tcp"}) + c.Assert(req.Form["IpPermissions.1.FromPort"], DeepEquals, []string{"80"}) + c.Assert(req.Form["IpPermissions.1.ToPort"], DeepEquals, []string{"81"}) + c.Assert(req.Form["IpPermissions.1.Groups.1.UserId"], DeepEquals, []string{"999988887777"}) + c.Assert(req.Form["IpPermissions.1.Groups.1.GroupName"], DeepEquals, []string{"OtherAccountGroup"}) + c.Assert(req.Form["IpPermissions.1.Groups.2.UserId"], IsNil) + c.Assert(req.Form["IpPermissions.1.Groups.2.GroupName"], IsNil) + c.Assert(req.Form["IpPermissions.1.Groups.2.GroupId"], DeepEquals, []string{"sg-67ad940e"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestRevokeSecurityGroupExample(c *C) { + // RevokeSecurityGroup is implemented by the same code as AuthorizeSecurityGroup + // so there's no need to duplicate all the tests. + testServer.Response(200, nil, RevokeSecurityGroupIngressExample) + + resp, err := s.ec2.RevokeSecurityGroup(ec2.SecurityGroup{Name: "websrv"}, nil) + + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"RevokeSecurityGroupIngress"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestCreateTags(c *C) { + testServer.Response(200, nil, CreateTagsExample) + + resp, err := s.ec2.CreateTags([]string{"ami-1a2b3c4d", "i-7f4d3a2b"}, []ec2.Tag{{"webserver", ""}, {"stack", "Production"}}) + + req := testServer.WaitRequest() + c.Assert(req.Form["ResourceId.1"], DeepEquals, []string{"ami-1a2b3c4d"}) + c.Assert(req.Form["ResourceId.2"], DeepEquals, []string{"i-7f4d3a2b"}) + c.Assert(req.Form["Tag.1.Key"], DeepEquals, []string{"webserver"}) + c.Assert(req.Form["Tag.1.Value"], DeepEquals, []string{""}) + c.Assert(req.Form["Tag.2.Key"], DeepEquals, []string{"stack"}) + c.Assert(req.Form["Tag.2.Value"], DeepEquals, []string{"Production"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestStartInstances(c *C) { + testServer.Response(200, nil, StartInstancesExample) + + resp, err := s.ec2.StartInstances("i-10a64379") + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"StartInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-10a64379"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + + s0 := resp.StateChanges[0] + c.Assert(s0.InstanceId, Equals, "i-10a64379") + c.Assert(s0.CurrentState.Code, Equals, 0) + c.Assert(s0.CurrentState.Name, Equals, "pending") + c.Assert(s0.PreviousState.Code, Equals, 80) + c.Assert(s0.PreviousState.Name, Equals, "stopped") +} + +func (s *S) TestStopInstances(c *C) { + testServer.Response(200, nil, StopInstancesExample) + + resp, err := s.ec2.StopInstances("i-10a64379") + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"StopInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-10a64379"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + + s0 := resp.StateChanges[0] + c.Assert(s0.InstanceId, Equals, "i-10a64379") + c.Assert(s0.CurrentState.Code, Equals, 64) + c.Assert(s0.CurrentState.Name, Equals, "stopping") + c.Assert(s0.PreviousState.Code, Equals, 16) + c.Assert(s0.PreviousState.Name, Equals, "running") +} + +func (s *S) TestRebootInstances(c *C) { + testServer.Response(200, nil, RebootInstancesExample) + + resp, err := s.ec2.RebootInstances("i-10a64379") + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"RebootInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-10a64379"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestSignatureWithEndpointPath(c *C) { + ec2.FakeTime(true) + defer ec2.FakeTime(false) + + testServer.Response(200, nil, RebootInstancesExample) + + // https://bugs.launchpad.net/goamz/+bug/1022749 + ec2 := ec2.NewWithClient(s.ec2.Auth, aws.Region{EC2Endpoint: testServer.URL + "/services/Cloud"}, testutil.DefaultClient) + + _, err := ec2.RebootInstances("i-10a64379") + c.Assert(err, IsNil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Signature"], DeepEquals, []string{"tyOTQ0c0T5ujskCPTWa5ATMtv7UyErgT339cU8O2+Q8="}) +} + +func (s *S) TestDescribeInstanceStatusExample(c *C) { + testServer.Response(200, nil, DescribeInstanceStatusExample) + options := &ec2.DescribeInstanceStatus{} + resp, err := s.ec2.DescribeInstanceStatus(options, nil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeInstanceStatus"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "3be1508e-c444-4fef-89cc-0b1223c4f02fEXAMPLE") + c.Assert(resp.InstanceStatus[0].InstanceId, Equals, "i-1a2b3c4d") + c.Assert(resp.InstanceStatus[0].InstanceState.Code, Equals, 16) + c.Assert(resp.InstanceStatus[0].SystemStatus.Status, Equals, "impaired") + c.Assert(resp.InstanceStatus[0].SystemStatus.Details[0].Name, Equals, "reachability") + c.Assert(resp.InstanceStatus[0].SystemStatus.Details[0].Status, Equals, "failed") + c.Assert(resp.InstanceStatus[0].SystemStatus.Details[0].ImpairedSince, Equals, "YYYY-MM-DDTHH:MM:SS.000Z") + c.Assert(resp.InstanceStatus[0].InstanceStatus.Details[0].Name, Equals, "reachability") + c.Assert(resp.InstanceStatus[0].InstanceStatus.Details[0].Status, Equals, "failed") + c.Assert(resp.InstanceStatus[0].InstanceStatus.Details[0].ImpairedSince, Equals, "YYYY-MM-DDTHH:MM:SS.000Z") + c.Assert(resp.InstanceStatus[0].Events[0].Code, Equals, "instance-retirement") + c.Assert(resp.InstanceStatus[0].Events[0].Description, Equals, "The instance is running on degraded hardware") + c.Assert(resp.InstanceStatus[0].Events[0].NotBefore, Equals, "YYYY-MM-DDTHH:MM:SS+0000") + c.Assert(resp.InstanceStatus[0].Events[0].NotAfter, Equals, "YYYY-MM-DDTHH:MM:SS+0000") +} + +func (s *S) TestAllocateAddressExample(c *C) { + testServer.Response(200, nil, AllocateAddressExample) + + options := &ec2.AllocateAddress{ + Domain: "vpc", + } + + resp, err := s.ec2.AllocateAddress(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"AllocateAddress"}) + c.Assert(req.Form["Domain"], DeepEquals, []string{"vpc"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.PublicIp, Equals, "198.51.100.1") + c.Assert(resp.Domain, Equals, "vpc") + c.Assert(resp.AllocationId, Equals, "eipalloc-5723d13e") +} + +func (s *S) TestReleaseAddressExample(c *C) { + testServer.Response(200, nil, ReleaseAddressExample) + + resp, err := s.ec2.ReleaseAddress("eipalloc-5723d13e") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"ReleaseAddress"}) + c.Assert(req.Form["AllocationId"], DeepEquals, []string{"eipalloc-5723d13e"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestAssociateAddressExample(c *C) { + testServer.Response(200, nil, AssociateAddressExample) + + options := &ec2.AssociateAddress{ + InstanceId: "i-4fd2431a", + AllocationId: "eipalloc-5723d13e", + AllowReassociation: true, + } + + resp, err := s.ec2.AssociateAddress(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"AssociateAddress"}) + c.Assert(req.Form["InstanceId"], DeepEquals, []string{"i-4fd2431a"}) + c.Assert(req.Form["AllocationId"], DeepEquals, []string{"eipalloc-5723d13e"}) + c.Assert(req.Form["AllowReassociation"], DeepEquals, []string{"true"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.AssociationId, Equals, "eipassoc-fc5ca095") +} + +func (s *S) TestDisassociateAddressExample(c *C) { + testServer.Response(200, nil, DisassociateAddressExample) + + resp, err := s.ec2.DisassociateAddress("eipassoc-aa7486c3") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DisassociateAddress"}) + c.Assert(req.Form["AssociationId"], DeepEquals, []string{"eipassoc-aa7486c3"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestModifyInstance(c *C) { + testServer.Response(200, nil, ModifyInstanceExample) + + options := ec2.ModifyInstance{ + InstanceType: "m1.small", + DisableAPITermination: true, + EbsOptimized: true, + SecurityGroups: []ec2.SecurityGroup{{Id: "g1"}, {Id: "g2"}}, + ShutdownBehavior: "terminate", + KernelId: "kernel-id", + RamdiskId: "ramdisk-id", + SourceDestCheck: true, + SriovNetSupport: true, + UserData: []byte("1234"), + BlockDevices: []ec2.BlockDeviceMapping{ + {DeviceName: "/dev/sda1", SnapshotId: "snap-a08912c9", DeleteOnTermination: true}, + }, + } + + resp, err := s.ec2.ModifyInstance("i-2ba64342", &options) + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"ModifyInstanceAttribute"}) + c.Assert(req.Form["InstanceId"], DeepEquals, []string{"i-2ba64342"}) + c.Assert(req.Form["InstanceType.Value"], DeepEquals, []string{"m1.small"}) + c.Assert(req.Form["BlockDeviceMapping.1.DeviceName"], DeepEquals, []string{"/dev/sda1"}) + c.Assert(req.Form["BlockDeviceMapping.1.Ebs.SnapshotId"], DeepEquals, []string{"snap-a08912c9"}) + c.Assert(req.Form["BlockDeviceMapping.1.Ebs.DeleteOnTermination"], DeepEquals, []string{"true"}) + c.Assert(req.Form["DisableApiTermination.Value"], DeepEquals, []string{"true"}) + c.Assert(req.Form["EbsOptimized"], DeepEquals, []string{"true"}) + c.Assert(req.Form["GroupId.1"], DeepEquals, []string{"g1"}) + c.Assert(req.Form["GroupId.2"], DeepEquals, []string{"g2"}) + c.Assert(req.Form["InstanceInitiatedShutdownBehavior.Value"], DeepEquals, []string{"terminate"}) + c.Assert(req.Form["Kernel.Value"], DeepEquals, []string{"kernel-id"}) + c.Assert(req.Form["Ramdisk.Value"], DeepEquals, []string{"ramdisk-id"}) + c.Assert(req.Form["SourceDestCheck.Value"], DeepEquals, []string{"true"}) + c.Assert(req.Form["SriovNetSupport.Value"], DeepEquals, []string{"simple"}) + c.Assert(req.Form["UserData"], DeepEquals, []string{"MTIzNA=="}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestCreateVpc(c *C) { + testServer.Response(200, nil, CreateVpcExample) + + options := &ec2.CreateVpc{ + CidrBlock: "foo", + } + + resp, err := s.ec2.CreateVpc(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["CidrBlock"], DeepEquals, []string{"foo"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE") + c.Assert(resp.VPC.VpcId, Equals, "vpc-1a2b3c4d") + c.Assert(resp.VPC.State, Equals, "pending") + c.Assert(resp.VPC.CidrBlock, Equals, "10.0.0.0/16") + c.Assert(resp.VPC.DHCPOptionsID, Equals, "dopt-1a2b3c4d2") + c.Assert(resp.VPC.InstanceTenancy, Equals, "default") +} + +func (s *S) TestDescribeVpcs(c *C) { + testServer.Response(200, nil, DescribeVpcsExample) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.DescribeVpcs([]string{"id1", "id2"}, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeVpcs"}) + c.Assert(req.Form["VpcId.1"], DeepEquals, []string{"id1"}) + c.Assert(req.Form["VpcId.2"], DeepEquals, []string{"id2"}) + c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"}) + c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"}) + c.Assert(req.Form["Filter.1.Value.2"], IsNil) + c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"}) + c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"}) + c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE") + c.Assert(resp.VPCs, HasLen, 1) +} + +func (s *S) TestCreateSubnet(c *C) { + testServer.Response(200, nil, CreateSubnetExample) + + options := &ec2.CreateSubnet{ + AvailabilityZone: "baz", + CidrBlock: "foo", + VpcId: "bar", + } + + resp, err := s.ec2.CreateSubnet(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["VpcId"], DeepEquals, []string{"bar"}) + c.Assert(req.Form["CidrBlock"], DeepEquals, []string{"foo"}) + c.Assert(req.Form["AvailabilityZone"], DeepEquals, []string{"baz"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE") + c.Assert(resp.Subnet.SubnetId, Equals, "subnet-9d4a7b6c") + c.Assert(resp.Subnet.State, Equals, "pending") + c.Assert(resp.Subnet.VpcId, Equals, "vpc-1a2b3c4d") + c.Assert(resp.Subnet.CidrBlock, Equals, "10.0.1.0/24") + c.Assert(resp.Subnet.AvailableIpAddressCount, Equals, 251) +} + +func (s *S) TestModifySubnetAttribute(c *C) { + testServer.Response(200, nil, ModifySubnetAttributeExample) + + options := &ec2.ModifySubnetAttribute{ + SubnetId: "foo", + MapPublicIpOnLaunch: true, + } + + resp, err := s.ec2.ModifySubnetAttribute(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["SubnetId"], DeepEquals, []string{"foo"}) + c.Assert(req.Form["MapPublicIpOnLaunch.Value"], DeepEquals, []string{"true"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestResetImageAttribute(c *C) { + testServer.Response(200, nil, ResetImageAttributeExample) + + options := ec2.ResetImageAttribute{Attribute: "launchPermission"} + resp, err := s.ec2.ResetImageAttribute("i-2ba64342", &options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"ResetImageAttribute"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestDescribeAvailabilityZonesExample1(c *C) { + testServer.Response(200, nil, DescribeAvailabilityZonesExample1) + + resp, err := s.ec2.DescribeAvailabilityZones(nil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeAvailabilityZones"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Zones, HasLen, 4) + + z0 := resp.Zones[0] + c.Assert(z0.Name, Equals, "us-east-1a") + c.Assert(z0.Region, Equals, "us-east-1") + c.Assert(z0.State, Equals, "available") + c.Assert(z0.MessageSet, HasLen, 0) + + z1 := resp.Zones[1] + c.Assert(z1.Name, Equals, "us-east-1b") + c.Assert(z1.Region, Equals, "us-east-1") + c.Assert(z1.State, Equals, "available") + c.Assert(z1.MessageSet, HasLen, 0) + + z2 := resp.Zones[2] + c.Assert(z2.Name, Equals, "us-east-1c") + c.Assert(z2.Region, Equals, "us-east-1") + c.Assert(z2.State, Equals, "available") + c.Assert(z2.MessageSet, HasLen, 0) + + z3 := resp.Zones[3] + c.Assert(z3.Name, Equals, "us-east-1d") + c.Assert(z3.Region, Equals, "us-east-1") + c.Assert(z3.State, Equals, "available") + c.Assert(z3.MessageSet, HasLen, 0) +} + +func (s *S) TestDescribeAvailabilityZonesExample2(c *C) { + testServer.Response(200, nil, DescribeAvailabilityZonesExample2) + + resp, err := s.ec2.DescribeAvailabilityZones(nil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeAvailabilityZones"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Zones, HasLen, 2) + + z0 := resp.Zones[0] + c.Assert(z0.Name, Equals, "us-east-1a") + c.Assert(z0.Region, Equals, "us-east-1") + c.Assert(z0.State, Equals, "impaired") + c.Assert(z0.MessageSet, HasLen, 0) + + z1 := resp.Zones[1] + c.Assert(z1.Name, Equals, "us-east-1b") + c.Assert(z1.Region, Equals, "us-east-1") + c.Assert(z1.State, Equals, "unavailable") + c.Assert(z1.MessageSet, DeepEquals, []string{"us-east-1b is currently down for maintenance."}) +} + +func (s *S) TestCreateNetworkAcl(c *C) { + testServer.Response(200, nil, CreateNetworkAclExample) + + options := &ec2.CreateNetworkAcl{ + VpcId: "vpc-11ad4878", + } + + resp, err := s.ec2.CreateNetworkAcl(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["VpcId"], DeepEquals, []string{"vpc-11ad4878"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.NetworkAcl.VpcId, Equals, "vpc-11ad4878") + c.Assert(resp.NetworkAcl.NetworkAclId, Equals, "acl-5fb85d36") + c.Assert(resp.NetworkAcl.Default, Equals, "false") + c.Assert(resp.NetworkAcl.EntrySet, HasLen, 2) + c.Assert(resp.NetworkAcl.EntrySet[0].RuleNumber, Equals, 32767) + c.Assert(resp.NetworkAcl.EntrySet[0].Protocol, Equals, -1) + c.Assert(resp.NetworkAcl.EntrySet[0].RuleAction, Equals, "deny") + c.Assert(resp.NetworkAcl.EntrySet[0].Egress, Equals, true) + c.Assert(resp.NetworkAcl.EntrySet[0].CidrBlock, Equals, "0.0.0.0/0") +} + +func (s *S) TestCreateNetworkAclEntry(c *C) { + testServer.Response(200, nil, CreateNetworkAclEntryRespExample) + + options := &ec2.NetworkAclEntry{ + RuleNumber: 32767, + Protocol: 6, + RuleAction: "deny", + Egress: true, + CidrBlock: "0.0.0.0/0", + PortRange: ec2.PortRange{ + To: 22, + From: 22, + }, + } + + resp, err := s.ec2.CreateNetworkAclEntry("acl-11ad4878", options) + + req := testServer.WaitRequest() + + c.Assert(req.Form["NetworkAclId"], DeepEquals, []string{"acl-11ad4878"}) + c.Assert(req.Form["RuleNumber"], DeepEquals, []string{"32767"}) + c.Assert(req.Form["Protocol"], DeepEquals, []string{"6"}) + c.Assert(req.Form["RuleAction"], DeepEquals, []string{"deny"}) + c.Assert(req.Form["Egress"], DeepEquals, []string{"true"}) + c.Assert(req.Form["CidrBlock"], DeepEquals, []string{"0.0.0.0/0"}) + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestDescribeNetworkAcls(c *C) { + testServer.Response(200, nil, DescribeNetworkAclsExample) + + filter := ec2.NewFilter() + filter.Add("vpc-id", "vpc-5266953b") + + resp, err := s.ec2.NetworkAcls([]string{"acl-5566953c", "acl-5d659634"}, filter) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.NetworkAcls, HasLen, 2) + c.Assert(resp.NetworkAcls[1].AssociationSet, HasLen, 2) + c.Assert(resp.NetworkAcls[1].AssociationSet[0].NetworkAclAssociationId, Equals, "aclassoc-5c659635") + c.Assert(resp.NetworkAcls[1].AssociationSet[0].NetworkAclId, Equals, "acl-5d659634") + c.Assert(resp.NetworkAcls[1].AssociationSet[0].SubnetId, Equals, "subnet-ff669596") +} + +func (s *S) TestReplaceNetworkAclAssociation(c *C) { + testServer.Response(200, nil, ReplaceNetworkAclAssociationResponseExample) + + resp, err := s.ec2.ReplaceNetworkAclAssociation("aclassoc-e5b95c8c", "acl-5fb85d36") + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.NewAssociationId, Equals, "aclassoc-17b85d7e") +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2i_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2i_test.go new file mode 100644 index 000000000000..8b025dfb408a --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2i_test.go @@ -0,0 +1,204 @@ +package ec2_test + +import ( + "crypto/rand" + "fmt" + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/goamz/testutil" + . "github.com/motain/gocheck" +) + +// AmazonServer represents an Amazon EC2 server. +type AmazonServer struct { + auth aws.Auth +} + +func (s *AmazonServer) SetUp(c *C) { + auth, err := aws.EnvAuth() + if err != nil { + c.Fatal(err.Error()) + } + s.auth = auth +} + +// Suite cost per run: 0.02 USD +var _ = Suite(&AmazonClientSuite{}) + +// AmazonClientSuite tests the client against a live EC2 server. +type AmazonClientSuite struct { + srv AmazonServer + ClientTests +} + +func (s *AmazonClientSuite) SetUpSuite(c *C) { + if !testutil.Amazon { + c.Skip("AmazonClientSuite tests not enabled") + } + s.srv.SetUp(c) + s.ec2 = ec2.NewWithClient(s.srv.auth, aws.USEast, testutil.DefaultClient) +} + +// ClientTests defines integration tests designed to test the client. +// It is not used as a test suite in itself, but embedded within +// another type. +type ClientTests struct { + ec2 *ec2.EC2 +} + +var imageId = "ami-ccf405a5" // Ubuntu Maverick, i386, EBS store + +// Cost: 0.00 USD +func (s *ClientTests) TestRunInstancesError(c *C) { + options := ec2.RunInstances{ + ImageId: "ami-a6f504cf", // Ubuntu Maverick, i386, instance store + InstanceType: "t1.micro", // Doesn't work with micro, results in 400. + } + + resp, err := s.ec2.RunInstances(&options) + + c.Assert(resp, IsNil) + c.Assert(err, ErrorMatches, "AMI.*root device.*not supported.*") + + ec2err, ok := err.(*ec2.Error) + c.Assert(ok, Equals, true) + c.Assert(ec2err.StatusCode, Equals, 400) + c.Assert(ec2err.Code, Equals, "UnsupportedOperation") + c.Assert(ec2err.Message, Matches, "AMI.*root device.*not supported.*") + c.Assert(ec2err.RequestId, Matches, ".+") +} + +// Cost: 0.02 USD +func (s *ClientTests) TestRunAndTerminate(c *C) { + options := ec2.RunInstances{ + ImageId: imageId, + InstanceType: "t1.micro", + } + resp1, err := s.ec2.RunInstances(&options) + c.Assert(err, IsNil) + c.Check(resp1.ReservationId, Matches, "r-[0-9a-f]*") + c.Check(resp1.OwnerId, Matches, "[0-9]+") + c.Check(resp1.Instances, HasLen, 1) + c.Check(resp1.Instances[0].InstanceType, Equals, "t1.micro") + + instId := resp1.Instances[0].InstanceId + + resp2, err := s.ec2.Instances([]string{instId}, nil) + c.Assert(err, IsNil) + if c.Check(resp2.Reservations, HasLen, 1) && c.Check(len(resp2.Reservations[0].Instances), Equals, 1) { + inst := resp2.Reservations[0].Instances[0] + c.Check(inst.InstanceId, Equals, instId) + } + + resp3, err := s.ec2.TerminateInstances([]string{instId}) + c.Assert(err, IsNil) + c.Check(resp3.StateChanges, HasLen, 1) + c.Check(resp3.StateChanges[0].InstanceId, Equals, instId) + c.Check(resp3.StateChanges[0].CurrentState.Name, Equals, "shutting-down") + c.Check(resp3.StateChanges[0].CurrentState.Code, Equals, 32) +} + +// Cost: 0.00 USD +func (s *ClientTests) TestSecurityGroups(c *C) { + name := "goamz-test" + descr := "goamz security group for tests" + + // Clean it up, if a previous test left it around and avoid leaving it around. + s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + defer s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + + resp1, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr}) + c.Assert(err, IsNil) + c.Assert(resp1.RequestId, Matches, ".+") + c.Assert(resp1.Name, Equals, name) + c.Assert(resp1.Id, Matches, ".+") + + resp1, err = s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr}) + ec2err, _ := err.(*ec2.Error) + c.Assert(resp1, IsNil) + c.Assert(ec2err, NotNil) + c.Assert(ec2err.Code, Equals, "InvalidGroup.Duplicate") + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 0, + ToPort: 1024, + SourceIPs: []string{"127.0.0.1/24"}, + }} + + resp2, err := s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms) + c.Assert(err, IsNil) + c.Assert(resp2.RequestId, Matches, ".+") + + resp3, err := s.ec2.SecurityGroups(ec2.SecurityGroupNames(name), nil) + c.Assert(err, IsNil) + c.Assert(resp3.RequestId, Matches, ".+") + c.Assert(resp3.Groups, HasLen, 1) + + g0 := resp3.Groups[0] + c.Assert(g0.Name, Equals, name) + c.Assert(g0.Description, Equals, descr) + c.Assert(g0.IPPerms, HasLen, 1) + c.Assert(g0.IPPerms[0].Protocol, Equals, "tcp") + c.Assert(g0.IPPerms[0].FromPort, Equals, 0) + c.Assert(g0.IPPerms[0].ToPort, Equals, 1024) + c.Assert(g0.IPPerms[0].SourceIPs, DeepEquals, []string{"127.0.0.1/24"}) + + resp2, err = s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + c.Assert(err, IsNil) + c.Assert(resp2.RequestId, Matches, ".+") +} + +var sessionId = func() string { + buf := make([]byte, 8) + // if we have no randomness, we'll just make do, so ignore the error. + rand.Read(buf) + return fmt.Sprintf("%x", buf) +}() + +// sessionName reutrns a name that is probably +// unique to this test session. +func sessionName(prefix string) string { + return prefix + "-" + sessionId +} + +var allRegions = []aws.Region{ + aws.USEast, + aws.USWest, + aws.EUWest, + aws.EUCentral, + aws.APSoutheast, + aws.APNortheast, +} + +// Communicate with all EC2 endpoints to see if they are alive. +func (s *ClientTests) TestRegions(c *C) { + name := sessionName("goamz-region-test") + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 80, + ToPort: 80, + SourceIPs: []string{"127.0.0.1/32"}, + }} + errs := make(chan error, len(allRegions)) + for _, region := range allRegions { + go func(r aws.Region) { + e := ec2.NewWithClient(s.ec2.Auth, r, testutil.DefaultClient) + _, err := e.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms) + errs <- err + }(region) + } + for _ = range allRegions { + err := <-errs + if err != nil { + ec2_err, ok := err.(*ec2.Error) + if ok { + c.Check(ec2_err.Code, Matches, "InvalidGroup.NotFound") + } else { + c.Errorf("Non-EC2 error: %s", err) + } + } else { + c.Errorf("Test should have errored but it seems to have succeeded") + } + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2t_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2t_test.go new file mode 100644 index 000000000000..fe50356f9085 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2t_test.go @@ -0,0 +1,580 @@ +package ec2_test + +import ( + "fmt" + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/goamz/ec2/ec2test" + "github.com/mitchellh/goamz/testutil" + . "github.com/motain/gocheck" + "regexp" + "sort" +) + +// LocalServer represents a local ec2test fake server. +type LocalServer struct { + auth aws.Auth + region aws.Region + srv *ec2test.Server +} + +func (s *LocalServer) SetUp(c *C) { + srv, err := ec2test.NewServer() + c.Assert(err, IsNil) + c.Assert(srv, NotNil) + + s.srv = srv + s.region = aws.Region{EC2Endpoint: srv.URL()} +} + +// LocalServerSuite defines tests that will run +// against the local ec2test server. It includes +// selected tests from ClientTests; +// when the ec2test functionality is sufficient, it should +// include all of them, and ClientTests can be simply embedded. +type LocalServerSuite struct { + srv LocalServer + ServerTests + clientTests ClientTests +} + +var _ = Suite(&LocalServerSuite{}) + +func (s *LocalServerSuite) SetUpSuite(c *C) { + s.srv.SetUp(c) + s.ServerTests.ec2 = ec2.NewWithClient(s.srv.auth, s.srv.region, testutil.DefaultClient) + s.clientTests.ec2 = ec2.NewWithClient(s.srv.auth, s.srv.region, testutil.DefaultClient) +} + +func (s *LocalServerSuite) TestRunAndTerminate(c *C) { + s.clientTests.TestRunAndTerminate(c) +} + +func (s *LocalServerSuite) TestSecurityGroups(c *C) { + s.clientTests.TestSecurityGroups(c) +} + +// TestUserData is not defined on ServerTests because it +// requires the ec2test server to function. +func (s *LocalServerSuite) TestUserData(c *C) { + data := make([]byte, 256) + for i := range data { + data[i] = byte(i) + } + inst, err := s.ec2.RunInstances(&ec2.RunInstances{ + ImageId: imageId, + InstanceType: "t1.micro", + UserData: data, + }) + c.Assert(err, IsNil) + c.Assert(inst, NotNil) + c.Assert(inst.Instances[0].DNSName, Equals, inst.Instances[0].InstanceId+".example.com") + + id := inst.Instances[0].InstanceId + + defer s.ec2.TerminateInstances([]string{id}) + + tinst := s.srv.srv.Instance(id) + c.Assert(tinst, NotNil) + c.Assert(tinst.UserData, DeepEquals, data) +} + +// AmazonServerSuite runs the ec2test server tests against a live EC2 server. +// It will only be activated if the -all flag is specified. +type AmazonServerSuite struct { + srv AmazonServer + ServerTests +} + +var _ = Suite(&AmazonServerSuite{}) + +func (s *AmazonServerSuite) SetUpSuite(c *C) { + if !testutil.Amazon { + c.Skip("AmazonServerSuite tests not enabled") + } + s.srv.SetUp(c) + s.ServerTests.ec2 = ec2.NewWithClient(s.srv.auth, aws.USEast, testutil.DefaultClient) +} + +// ServerTests defines a set of tests designed to test +// the ec2test local fake ec2 server. +// It is not used as a test suite in itself, but embedded within +// another type. +type ServerTests struct { + ec2 *ec2.EC2 +} + +func terminateInstances(c *C, e *ec2.EC2, insts []*ec2.Instance) { + var ids []string + for _, inst := range insts { + if inst != nil { + ids = append(ids, inst.InstanceId) + } + } + _, err := e.TerminateInstances(ids) + c.Check(err, IsNil, Commentf("%d INSTANCES LEFT RUNNING!!!", len(ids))) +} + +func (s *ServerTests) makeTestGroup(c *C, name, descr string) ec2.SecurityGroup { + // Clean it up if a previous test left it around. + _, err := s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + if err != nil && err.(*ec2.Error).Code != "InvalidGroup.NotFound" { + c.Fatalf("delete security group: %v", err) + } + + resp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr}) + c.Assert(err, IsNil) + c.Assert(resp.Name, Equals, name) + return resp.SecurityGroup +} + +func (s *ServerTests) TestIPPerms(c *C) { + g0 := s.makeTestGroup(c, "goamz-test0", "ec2test group 0") + defer s.ec2.DeleteSecurityGroup(g0) + + g1 := s.makeTestGroup(c, "goamz-test1", "ec2test group 1") + defer s.ec2.DeleteSecurityGroup(g1) + + resp, err := s.ec2.SecurityGroups([]ec2.SecurityGroup{g0, g1}, nil) + c.Assert(err, IsNil) + c.Assert(resp.Groups, HasLen, 2) + c.Assert(resp.Groups[0].IPPerms, HasLen, 0) + c.Assert(resp.Groups[1].IPPerms, HasLen, 0) + + ownerId := resp.Groups[0].OwnerId + + // test some invalid parameters + // TODO more + _, err = s.ec2.AuthorizeSecurityGroup(g0, []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 0, + ToPort: 1024, + SourceIPs: []string{"z127.0.0.1/24"}, + }}) + c.Assert(err, NotNil) + c.Check(err.(*ec2.Error).Code, Equals, "InvalidPermission.Malformed") + + // Check that AuthorizeSecurityGroup adds the correct authorizations. + _, err = s.ec2.AuthorizeSecurityGroup(g0, []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 2000, + ToPort: 2001, + SourceIPs: []string{"127.0.0.0/24"}, + SourceGroups: []ec2.UserSecurityGroup{{ + Name: g1.Name, + }, { + Id: g0.Id, + }}, + }, { + Protocol: "tcp", + FromPort: 2000, + ToPort: 2001, + SourceIPs: []string{"200.1.1.34/32"}, + }}) + c.Assert(err, IsNil) + + resp, err = s.ec2.SecurityGroups([]ec2.SecurityGroup{g0}, nil) + c.Assert(err, IsNil) + c.Assert(resp.Groups, HasLen, 1) + c.Assert(resp.Groups[0].IPPerms, HasLen, 1) + + perm := resp.Groups[0].IPPerms[0] + srcg := perm.SourceGroups + c.Assert(srcg, HasLen, 2) + + // Normalize so we don't care about returned order. + if srcg[0].Name == g1.Name { + srcg[0], srcg[1] = srcg[1], srcg[0] + } + c.Check(srcg[0].Name, Equals, g0.Name) + c.Check(srcg[0].Id, Equals, g0.Id) + c.Check(srcg[0].OwnerId, Equals, ownerId) + c.Check(srcg[1].Name, Equals, g1.Name) + c.Check(srcg[1].Id, Equals, g1.Id) + c.Check(srcg[1].OwnerId, Equals, ownerId) + + sort.Strings(perm.SourceIPs) + c.Check(perm.SourceIPs, DeepEquals, []string{"127.0.0.0/24", "200.1.1.34/32"}) + + // Check that we can't delete g1 (because g0 is using it) + _, err = s.ec2.DeleteSecurityGroup(g1) + c.Assert(err, NotNil) + c.Check(err.(*ec2.Error).Code, Equals, "InvalidGroup.InUse") + + _, err = s.ec2.RevokeSecurityGroup(g0, []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 2000, + ToPort: 2001, + SourceGroups: []ec2.UserSecurityGroup{{Id: g1.Id}}, + }, { + Protocol: "tcp", + FromPort: 2000, + ToPort: 2001, + SourceIPs: []string{"200.1.1.34/32"}, + }}) + c.Assert(err, IsNil) + + resp, err = s.ec2.SecurityGroups([]ec2.SecurityGroup{g0}, nil) + c.Assert(err, IsNil) + c.Assert(resp.Groups, HasLen, 1) + c.Assert(resp.Groups[0].IPPerms, HasLen, 1) + + perm = resp.Groups[0].IPPerms[0] + srcg = perm.SourceGroups + c.Assert(srcg, HasLen, 1) + c.Check(srcg[0].Name, Equals, g0.Name) + c.Check(srcg[0].Id, Equals, g0.Id) + c.Check(srcg[0].OwnerId, Equals, ownerId) + + c.Check(perm.SourceIPs, DeepEquals, []string{"127.0.0.0/24"}) + + // We should be able to delete g1 now because we've removed its only use. + _, err = s.ec2.DeleteSecurityGroup(g1) + c.Assert(err, IsNil) + + _, err = s.ec2.DeleteSecurityGroup(g0) + c.Assert(err, IsNil) + + f := ec2.NewFilter() + f.Add("group-id", g0.Id, g1.Id) + resp, err = s.ec2.SecurityGroups(nil, f) + c.Assert(err, IsNil) + c.Assert(resp.Groups, HasLen, 0) +} + +func (s *ServerTests) TestDuplicateIPPerm(c *C) { + name := "goamz-test" + descr := "goamz security group for tests" + + // Clean it up, if a previous test left it around and avoid leaving it around. + s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + defer s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + + resp1, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr}) + c.Assert(err, IsNil) + c.Assert(resp1.Name, Equals, name) + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 200, + ToPort: 1024, + SourceIPs: []string{"127.0.0.1/24"}, + }, { + Protocol: "tcp", + FromPort: 0, + ToPort: 100, + SourceIPs: []string{"127.0.0.1/24"}, + }} + + _, err = s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms[0:1]) + c.Assert(err, IsNil) + + _, err = s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms[0:2]) + c.Assert(err, ErrorMatches, `.*\(InvalidPermission.Duplicate\)`) +} + +type filterSpec struct { + name string + values []string +} + +func (s *ServerTests) TestInstanceFiltering(c *C) { + groupResp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: sessionName("testgroup1"), Description: "testgroup one description"}) + c.Assert(err, IsNil) + group1 := groupResp.SecurityGroup + defer s.ec2.DeleteSecurityGroup(group1) + + groupResp, err = s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: sessionName("testgroup2"), Description: "testgroup two description"}) + c.Assert(err, IsNil) + group2 := groupResp.SecurityGroup + defer s.ec2.DeleteSecurityGroup(group2) + + insts := make([]*ec2.Instance, 3) + inst, err := s.ec2.RunInstances(&ec2.RunInstances{ + MinCount: 2, + ImageId: imageId, + InstanceType: "t1.micro", + SecurityGroups: []ec2.SecurityGroup{group1}, + }) + c.Assert(err, IsNil) + insts[0] = &inst.Instances[0] + insts[1] = &inst.Instances[1] + defer terminateInstances(c, s.ec2, insts) + + imageId2 := "ami-e358958a" // Natty server, i386, EBS store + inst, err = s.ec2.RunInstances(&ec2.RunInstances{ + ImageId: imageId2, + InstanceType: "t1.micro", + SecurityGroups: []ec2.SecurityGroup{group2}, + }) + c.Assert(err, IsNil) + insts[2] = &inst.Instances[0] + + ids := func(indices ...int) (instIds []string) { + for _, index := range indices { + instIds = append(instIds, insts[index].InstanceId) + } + return + } + + tests := []struct { + about string + instanceIds []string // instanceIds argument to Instances method. + filters []filterSpec // filters argument to Instances method. + resultIds []string // set of instance ids of expected results. + allowExtra bool // resultIds may be incomplete. + err string // expected error. + }{ + { + about: "check that Instances returns all instances", + resultIds: ids(0, 1, 2), + allowExtra: true, + }, { + about: "check that specifying two instance ids returns them", + instanceIds: ids(0, 2), + resultIds: ids(0, 2), + }, { + about: "check that specifying a non-existent instance id gives an error", + instanceIds: append(ids(0), "i-deadbeef"), + err: `.*\(InvalidInstanceID\.NotFound\)`, + }, { + about: "check that a filter allowed both instances returns both of them", + filters: []filterSpec{ + {"instance-id", ids(0, 2)}, + }, + resultIds: ids(0, 2), + }, { + about: "check that a filter allowing only one instance returns it", + filters: []filterSpec{ + {"instance-id", ids(1)}, + }, + resultIds: ids(1), + }, { + about: "check that a filter allowing no instances returns none", + filters: []filterSpec{ + {"instance-id", []string{"i-deadbeef12345"}}, + }, + }, { + about: "check that filtering on group id works", + filters: []filterSpec{ + {"group-id", []string{group1.Id}}, + }, + resultIds: ids(0, 1), + }, { + about: "check that filtering on group name works", + filters: []filterSpec{ + {"group-name", []string{group1.Name}}, + }, + resultIds: ids(0, 1), + }, { + about: "check that filtering on image id works", + filters: []filterSpec{ + {"image-id", []string{imageId}}, + }, + resultIds: ids(0, 1), + allowExtra: true, + }, { + about: "combination filters 1", + filters: []filterSpec{ + {"image-id", []string{imageId, imageId2}}, + {"group-name", []string{group1.Name}}, + }, + resultIds: ids(0, 1), + }, { + about: "combination filters 2", + filters: []filterSpec{ + {"image-id", []string{imageId2}}, + {"group-name", []string{group1.Name}}, + }, + }, + } + for i, t := range tests { + c.Logf("%d. %s", i, t.about) + var f *ec2.Filter + if t.filters != nil { + f = ec2.NewFilter() + for _, spec := range t.filters { + f.Add(spec.name, spec.values...) + } + } + resp, err := s.ec2.Instances(t.instanceIds, f) + if t.err != "" { + c.Check(err, ErrorMatches, t.err) + continue + } + c.Assert(err, IsNil) + insts := make(map[string]*ec2.Instance) + for _, r := range resp.Reservations { + for j := range r.Instances { + inst := &r.Instances[j] + c.Check(insts[inst.InstanceId], IsNil, Commentf("duplicate instance id: %q", inst.InstanceId)) + insts[inst.InstanceId] = inst + } + } + if !t.allowExtra { + c.Check(insts, HasLen, len(t.resultIds), Commentf("expected %d instances got %#v", len(t.resultIds), insts)) + } + for j, id := range t.resultIds { + c.Check(insts[id], NotNil, Commentf("instance id %d (%q) not found; got %#v", j, id, insts)) + } + } +} + +func idsOnly(gs []ec2.SecurityGroup) []ec2.SecurityGroup { + for i := range gs { + gs[i].Name = "" + } + return gs +} + +func namesOnly(gs []ec2.SecurityGroup) []ec2.SecurityGroup { + for i := range gs { + gs[i].Id = "" + } + return gs +} + +func (s *ServerTests) TestGroupFiltering(c *C) { + g := make([]ec2.SecurityGroup, 4) + for i := range g { + resp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: sessionName(fmt.Sprintf("testgroup%d", i)), Description: fmt.Sprintf("testdescription%d", i)}) + c.Assert(err, IsNil) + g[i] = resp.SecurityGroup + c.Logf("group %d: %v", i, g[i]) + defer s.ec2.DeleteSecurityGroup(g[i]) + } + + perms := [][]ec2.IPPerm{ + {{ + Protocol: "tcp", + FromPort: 100, + ToPort: 200, + SourceIPs: []string{"1.2.3.4/32"}, + }}, + {{ + Protocol: "tcp", + FromPort: 200, + ToPort: 300, + SourceGroups: []ec2.UserSecurityGroup{{Id: g[1].Id}}, + }}, + {{ + Protocol: "udp", + FromPort: 200, + ToPort: 400, + SourceGroups: []ec2.UserSecurityGroup{{Id: g[1].Id}}, + }}, + } + for i, ps := range perms { + _, err := s.ec2.AuthorizeSecurityGroup(g[i], ps) + c.Assert(err, IsNil) + } + + groups := func(indices ...int) (gs []ec2.SecurityGroup) { + for _, index := range indices { + gs = append(gs, g[index]) + } + return + } + + type groupTest struct { + about string + groups []ec2.SecurityGroup // groupIds argument to SecurityGroups method. + filters []filterSpec // filters argument to SecurityGroups method. + results []ec2.SecurityGroup // set of expected result groups. + allowExtra bool // specified results may be incomplete. + err string // expected error. + } + filterCheck := func(name, val string, gs []ec2.SecurityGroup) groupTest { + return groupTest{ + about: "filter check " + name, + filters: []filterSpec{{name, []string{val}}}, + results: gs, + allowExtra: true, + } + } + tests := []groupTest{ + { + about: "check that SecurityGroups returns all groups", + results: groups(0, 1, 2, 3), + allowExtra: true, + }, { + about: "check that specifying two group ids returns them", + groups: idsOnly(groups(0, 2)), + results: groups(0, 2), + }, { + about: "check that specifying names only works", + groups: namesOnly(groups(0, 2)), + results: groups(0, 2), + }, { + about: "check that specifying a non-existent group id gives an error", + groups: append(groups(0), ec2.SecurityGroup{Id: "sg-eeeeeeeee"}), + err: `.*\(InvalidGroup\.NotFound\)`, + }, { + about: "check that a filter allowed two groups returns both of them", + filters: []filterSpec{ + {"group-id", []string{g[0].Id, g[2].Id}}, + }, + results: groups(0, 2), + }, + { + about: "check that the previous filter works when specifying a list of ids", + groups: groups(1, 2), + filters: []filterSpec{ + {"group-id", []string{g[0].Id, g[2].Id}}, + }, + results: groups(2), + }, { + about: "check that a filter allowing no groups returns none", + filters: []filterSpec{ + {"group-id", []string{"sg-eeeeeeeee"}}, + }, + }, + filterCheck("description", "testdescription1", groups(1)), + filterCheck("group-name", g[2].Name, groups(2)), + filterCheck("ip-permission.cidr", "1.2.3.4/32", groups(0)), + filterCheck("ip-permission.group-name", g[1].Name, groups(1, 2)), + filterCheck("ip-permission.protocol", "udp", groups(2)), + filterCheck("ip-permission.from-port", "200", groups(1, 2)), + filterCheck("ip-permission.to-port", "200", groups(0)), + // TODO owner-id + } + for i, t := range tests { + c.Logf("%d. %s", i, t.about) + var f *ec2.Filter + if t.filters != nil { + f = ec2.NewFilter() + for _, spec := range t.filters { + f.Add(spec.name, spec.values...) + } + } + resp, err := s.ec2.SecurityGroups(t.groups, f) + if t.err != "" { + c.Check(err, ErrorMatches, t.err) + continue + } + c.Assert(err, IsNil) + groups := make(map[string]*ec2.SecurityGroup) + for j := range resp.Groups { + group := &resp.Groups[j].SecurityGroup + c.Check(groups[group.Id], IsNil, Commentf("duplicate group id: %q", group.Id)) + + groups[group.Id] = group + } + // If extra groups may be returned, eliminate all groups that + // we did not create in this session apart from the default group. + if t.allowExtra { + namePat := regexp.MustCompile(sessionName("testgroup[0-9]")) + for id, g := range groups { + if !namePat.MatchString(g.Name) { + delete(groups, id) + } + } + } + c.Check(groups, HasLen, len(t.results)) + for j, g := range t.results { + rg := groups[g.Id] + c.Assert(rg, NotNil, Commentf("group %d (%v) not found; got %#v", j, g, groups)) + c.Check(rg.Name, Equals, g.Name, Commentf("group %d (%v)", j, g)) + } + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/filter.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/filter.go new file mode 100644 index 000000000000..1a0c0461937a --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/filter.go @@ -0,0 +1,84 @@ +package ec2test + +import ( + "fmt" + "net/url" + "strings" +) + +// filter holds an ec2 filter. A filter maps an attribute to a set of +// possible values for that attribute. For an item to pass through the +// filter, every attribute of the item mentioned in the filter must match +// at least one of its given values. +type filter map[string][]string + +// newFilter creates a new filter from the Filter fields in the url form. +// +// The filtering is specified through a map of name=>values, where the +// name is a well-defined key identifying the data to be matched, +// and the list of values holds the possible values the filtered +// item can take for the key to be included in the +// result set. For example: +// +// Filter.1.Name=instance-type +// Filter.1.Value.1=m1.small +// Filter.1.Value.2=m1.large +// +func newFilter(form url.Values) filter { + // TODO return an error if the fields are not well formed? + names := make(map[int]string) + values := make(map[int][]string) + maxId := 0 + for name, fvalues := range form { + var rest string + var id int + if x, _ := fmt.Sscanf(name, "Filter.%d.%s", &id, &rest); x != 2 { + continue + } + if id > maxId { + maxId = id + } + if rest == "Name" { + names[id] = fvalues[0] + continue + } + if !strings.HasPrefix(rest, "Value.") { + continue + } + values[id] = append(values[id], fvalues[0]) + } + + f := make(filter) + for id, name := range names { + f[name] = values[id] + } + return f +} + +func notDigit(r rune) bool { + return r < '0' || r > '9' +} + +// filterable represents an object that can be passed through a filter. +type filterable interface { + // matchAttr returns true if given attribute of the + // object matches value. It returns an error if the + // attribute is not recognised or the value is malformed. + matchAttr(attr, value string) (bool, error) +} + +// ok returns true if x passes through the filter. +func (f filter) ok(x filterable) (bool, error) { +next: + for a, vs := range f { + for _, v := range vs { + if ok, err := x.matchAttr(a, v); ok { + continue next + } else if err != nil { + return false, fmt.Errorf("bad attribute or value %q=%q for type %T: %v", a, v, x, err) + } + } + return false, nil + } + return true, nil +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/server.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/server.go new file mode 100644 index 000000000000..2f24cb2a2443 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/server.go @@ -0,0 +1,993 @@ +// The ec2test package implements a fake EC2 provider with +// the capability of inducing errors on any given operation, +// and retrospectively determining what operations have been +// carried out. +package ec2test + +import ( + "encoding/base64" + "encoding/xml" + "fmt" + "github.com/mitchellh/goamz/ec2" + "io" + "net" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "sync" +) + +var b64 = base64.StdEncoding + +// Action represents a request that changes the ec2 state. +type Action struct { + RequestId string + + // Request holds the requested action as a url.Values instance + Request url.Values + + // If the action succeeded, Response holds the value that + // was marshalled to build the XML response for the request. + Response interface{} + + // If the action failed, Err holds an error giving details of the failure. + Err *ec2.Error +} + +// TODO possible other things: +// - some virtual time stamp interface, so a client +// can ask for all actions after a certain virtual time. + +// Server implements an EC2 simulator for use in testing. +type Server struct { + url string + listener net.Listener + mu sync.Mutex + reqs []*Action + + instances map[string]*Instance // id -> instance + reservations map[string]*reservation // id -> reservation + groups map[string]*securityGroup // id -> group + maxId counter + reqId counter + reservationId counter + groupId counter + initialInstanceState ec2.InstanceState +} + +// reservation holds a simulated ec2 reservation. +type reservation struct { + id string + instances map[string]*Instance + groups []*securityGroup +} + +// instance holds a simulated ec2 instance +type Instance struct { + // UserData holds the data that was passed to the RunInstances request + // when the instance was started. + UserData []byte + id string + imageId string + reservation *reservation + instType string + state ec2.InstanceState +} + +// permKey represents permission for a given security +// group or IP address (but not both) to access a given range of +// ports. Equality of permKeys is used in the implementation of +// permission sets, relying on the uniqueness of securityGroup +// instances. +type permKey struct { + protocol string + fromPort int + toPort int + group *securityGroup + ipAddr string +} + +// securityGroup holds a simulated ec2 security group. +// Instances of securityGroup should only be created through +// Server.createSecurityGroup to ensure that groups can be +// compared by pointer value. +type securityGroup struct { + id string + name string + description string + + perms map[permKey]bool +} + +func (g *securityGroup) ec2SecurityGroup() ec2.SecurityGroup { + return ec2.SecurityGroup{ + Name: g.name, + Id: g.id, + } +} + +func (g *securityGroup) matchAttr(attr, value string) (ok bool, err error) { + switch attr { + case "description": + return g.description == value, nil + case "group-id": + return g.id == value, nil + case "group-name": + return g.name == value, nil + case "ip-permission.cidr": + return g.hasPerm(func(k permKey) bool { return k.ipAddr == value }), nil + case "ip-permission.group-name": + return g.hasPerm(func(k permKey) bool { + return k.group != nil && k.group.name == value + }), nil + case "ip-permission.from-port": + port, err := strconv.Atoi(value) + if err != nil { + return false, err + } + return g.hasPerm(func(k permKey) bool { return k.fromPort == port }), nil + case "ip-permission.to-port": + port, err := strconv.Atoi(value) + if err != nil { + return false, err + } + return g.hasPerm(func(k permKey) bool { return k.toPort == port }), nil + case "ip-permission.protocol": + return g.hasPerm(func(k permKey) bool { return k.protocol == value }), nil + case "owner-id": + return value == ownerId, nil + } + return false, fmt.Errorf("unknown attribute %q", attr) +} + +func (g *securityGroup) hasPerm(test func(k permKey) bool) bool { + for k := range g.perms { + if test(k) { + return true + } + } + return false +} + +// ec2Perms returns the list of EC2 permissions granted +// to g. It groups permissions by port range and protocol. +func (g *securityGroup) ec2Perms() (perms []ec2.IPPerm) { + // The grouping is held in result. We use permKey for convenience, + // (ensuring that the group and ipAddr of each key is zero). For + // each protocol/port range combination, we build up the permission + // set in the associated value. + result := make(map[permKey]*ec2.IPPerm) + for k := range g.perms { + groupKey := k + groupKey.group = nil + groupKey.ipAddr = "" + + ec2p := result[groupKey] + if ec2p == nil { + ec2p = &ec2.IPPerm{ + Protocol: k.protocol, + FromPort: k.fromPort, + ToPort: k.toPort, + } + result[groupKey] = ec2p + } + if k.group != nil { + ec2p.SourceGroups = append(ec2p.SourceGroups, + ec2.UserSecurityGroup{ + Id: k.group.id, + Name: k.group.name, + OwnerId: ownerId, + }) + } else { + ec2p.SourceIPs = append(ec2p.SourceIPs, k.ipAddr) + } + } + for _, ec2p := range result { + perms = append(perms, *ec2p) + } + return +} + +var actions = map[string]func(*Server, http.ResponseWriter, *http.Request, string) interface{}{ + "RunInstances": (*Server).runInstances, + "TerminateInstances": (*Server).terminateInstances, + "DescribeInstances": (*Server).describeInstances, + "CreateSecurityGroup": (*Server).createSecurityGroup, + "DescribeSecurityGroups": (*Server).describeSecurityGroups, + "DeleteSecurityGroup": (*Server).deleteSecurityGroup, + "AuthorizeSecurityGroupIngress": (*Server).authorizeSecurityGroupIngress, + "RevokeSecurityGroupIngress": (*Server).revokeSecurityGroupIngress, +} + +const ownerId = "9876" + +// newAction allocates a new action and adds it to the +// recorded list of server actions. +func (srv *Server) newAction() *Action { + srv.mu.Lock() + defer srv.mu.Unlock() + + a := new(Action) + srv.reqs = append(srv.reqs, a) + return a +} + +// NewServer returns a new server. +func NewServer() (*Server, error) { + srv := &Server{ + instances: make(map[string]*Instance), + groups: make(map[string]*securityGroup), + reservations: make(map[string]*reservation), + initialInstanceState: Pending, + } + + // Add default security group. + g := &securityGroup{ + name: "default", + description: "default group", + id: fmt.Sprintf("sg-%d", srv.groupId.next()), + } + g.perms = map[permKey]bool{ + permKey{ + protocol: "icmp", + fromPort: -1, + toPort: -1, + group: g, + }: true, + permKey{ + protocol: "tcp", + fromPort: 0, + toPort: 65535, + group: g, + }: true, + permKey{ + protocol: "udp", + fromPort: 0, + toPort: 65535, + group: g, + }: true, + } + srv.groups[g.id] = g + + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return nil, fmt.Errorf("cannot listen on localhost: %v", err) + } + srv.listener = l + + srv.url = "http://" + l.Addr().String() + + // we use HandlerFunc rather than *Server directly so that we + // can avoid exporting HandlerFunc from *Server. + go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + srv.serveHTTP(w, req) + })) + return srv, nil +} + +// Quit closes down the server. +func (srv *Server) Quit() { + srv.listener.Close() +} + +// SetInitialInstanceState sets the state that any new instances will be started in. +func (srv *Server) SetInitialInstanceState(state ec2.InstanceState) { + srv.mu.Lock() + srv.initialInstanceState = state + srv.mu.Unlock() +} + +// URL returns the URL of the server. +func (srv *Server) URL() string { + return srv.url +} + +// serveHTTP serves the EC2 protocol. +func (srv *Server) serveHTTP(w http.ResponseWriter, req *http.Request) { + req.ParseForm() + + a := srv.newAction() + a.RequestId = fmt.Sprintf("req%d", srv.reqId.next()) + a.Request = req.Form + + // Methods on Server that deal with parsing user data + // may fail. To save on error handling code, we allow these + // methods to call fatalf, which will panic with an *ec2.Error + // which will be caught here and returned + // to the client as a properly formed EC2 error. + defer func() { + switch err := recover().(type) { + case *ec2.Error: + a.Err = err + err.RequestId = a.RequestId + writeError(w, err) + case nil: + default: + panic(err) + } + }() + + f := actions[req.Form.Get("Action")] + if f == nil { + fatalf(400, "InvalidParameterValue", "Unrecognized Action") + } + + response := f(srv, w, req, a.RequestId) + a.Response = response + + w.Header().Set("Content-Type", `xml version="1.0" encoding="UTF-8"`) + xmlMarshal(w, response) +} + +// Instance returns the instance for the given instance id. +// It returns nil if there is no such instance. +func (srv *Server) Instance(id string) *Instance { + srv.mu.Lock() + defer srv.mu.Unlock() + return srv.instances[id] +} + +// writeError writes an appropriate error response. +// TODO how should we deal with errors when the +// error itself is potentially generated by backend-agnostic +// code? +func writeError(w http.ResponseWriter, err *ec2.Error) { + // Error encapsulates an error returned by EC2. + // TODO merge with ec2.Error when xml supports ignoring a field. + type ec2error struct { + Code string // EC2 error code ("UnsupportedOperation", ...) + Message string // The human-oriented error message + RequestId string + } + + type Response struct { + RequestId string + Errors []ec2error `xml:"Errors>Error"` + } + + w.Header().Set("Content-Type", `xml version="1.0" encoding="UTF-8"`) + w.WriteHeader(err.StatusCode) + xmlMarshal(w, Response{ + RequestId: err.RequestId, + Errors: []ec2error{{ + Code: err.Code, + Message: err.Message, + }}, + }) +} + +// xmlMarshal is the same as xml.Marshal except that +// it panics on error. The marshalling should not fail, +// but we want to know if it does. +func xmlMarshal(w io.Writer, x interface{}) { + if err := xml.NewEncoder(w).Encode(x); err != nil { + panic(fmt.Errorf("error marshalling %#v: %v", x, err)) + } +} + +// formToGroups parses a set of SecurityGroup form values +// as found in a RunInstances request, and returns the resulting +// slice of security groups. +// It calls fatalf if a group is not found. +func (srv *Server) formToGroups(form url.Values) []*securityGroup { + var groups []*securityGroup + for name, values := range form { + switch { + case strings.HasPrefix(name, "SecurityGroupId."): + if g := srv.groups[values[0]]; g != nil { + groups = append(groups, g) + } else { + fatalf(400, "InvalidGroup.NotFound", "unknown group id %q", values[0]) + } + case strings.HasPrefix(name, "SecurityGroup."): + var found *securityGroup + for _, g := range srv.groups { + if g.name == values[0] { + found = g + } + } + if found == nil { + fatalf(400, "InvalidGroup.NotFound", "unknown group name %q", values[0]) + } + groups = append(groups, found) + } + } + return groups +} + +// runInstances implements the EC2 RunInstances entry point. +func (srv *Server) runInstances(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + min := atoi(req.Form.Get("MinCount")) + max := atoi(req.Form.Get("MaxCount")) + if min < 0 || max < 1 { + fatalf(400, "InvalidParameterValue", "bad values for MinCount or MaxCount") + } + if min > max { + fatalf(400, "InvalidParameterCombination", "MinCount is greater than MaxCount") + } + var userData []byte + if data := req.Form.Get("UserData"); data != "" { + var err error + userData, err = b64.DecodeString(data) + if err != nil { + fatalf(400, "InvalidParameterValue", "bad UserData value: %v", err) + } + } + + // TODO attributes still to consider: + // ImageId: accept anything, we can verify later + // KeyName ? + // InstanceType ? + // KernelId ? + // RamdiskId ? + // AvailZone ? + // GroupName tag + // Monitoring ignore? + // SubnetId ? + // DisableAPITermination bool + // ShutdownBehavior string + // PrivateIPAddress string + + srv.mu.Lock() + defer srv.mu.Unlock() + + // make sure that form fields are correct before creating the reservation. + instType := req.Form.Get("InstanceType") + imageId := req.Form.Get("ImageId") + + r := srv.newReservation(srv.formToGroups(req.Form)) + + var resp ec2.RunInstancesResp + resp.RequestId = reqId + resp.ReservationId = r.id + resp.OwnerId = ownerId + + for i := 0; i < max; i++ { + inst := srv.newInstance(r, instType, imageId, srv.initialInstanceState) + inst.UserData = userData + resp.Instances = append(resp.Instances, inst.ec2instance()) + } + return &resp +} + +func (srv *Server) group(group ec2.SecurityGroup) *securityGroup { + if group.Id != "" { + return srv.groups[group.Id] + } + for _, g := range srv.groups { + if g.name == group.Name { + return g + } + } + return nil +} + +// NewInstances creates n new instances in srv with the given instance type, +// image ID, initial state and security groups. If any group does not already +// exist, it will be created. NewInstances returns the ids of the new instances. +func (srv *Server) NewInstances(n int, instType string, imageId string, state ec2.InstanceState, groups []ec2.SecurityGroup) []string { + srv.mu.Lock() + defer srv.mu.Unlock() + + rgroups := make([]*securityGroup, len(groups)) + for i, group := range groups { + g := srv.group(group) + if g == nil { + fatalf(400, "InvalidGroup.NotFound", "no such group %v", g) + } + rgroups[i] = g + } + r := srv.newReservation(rgroups) + + ids := make([]string, n) + for i := 0; i < n; i++ { + inst := srv.newInstance(r, instType, imageId, state) + ids[i] = inst.id + } + return ids +} + +func (srv *Server) newInstance(r *reservation, instType string, imageId string, state ec2.InstanceState) *Instance { + inst := &Instance{ + id: fmt.Sprintf("i-%d", srv.maxId.next()), + instType: instType, + imageId: imageId, + state: state, + reservation: r, + } + srv.instances[inst.id] = inst + r.instances[inst.id] = inst + return inst +} + +func (srv *Server) newReservation(groups []*securityGroup) *reservation { + r := &reservation{ + id: fmt.Sprintf("r-%d", srv.reservationId.next()), + instances: make(map[string]*Instance), + groups: groups, + } + + srv.reservations[r.id] = r + return r +} + +func (srv *Server) terminateInstances(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + srv.mu.Lock() + defer srv.mu.Unlock() + var resp ec2.TerminateInstancesResp + resp.RequestId = reqId + var insts []*Instance + for attr, vals := range req.Form { + if strings.HasPrefix(attr, "InstanceId.") { + id := vals[0] + inst := srv.instances[id] + if inst == nil { + fatalf(400, "InvalidInstanceID.NotFound", "no such instance id %q", id) + } + insts = append(insts, inst) + } + } + for _, inst := range insts { + resp.StateChanges = append(resp.StateChanges, inst.terminate()) + } + return &resp +} + +func (inst *Instance) terminate() (d ec2.InstanceStateChange) { + d.PreviousState = inst.state + inst.state = ShuttingDown + d.CurrentState = inst.state + d.InstanceId = inst.id + return d +} + +func (inst *Instance) ec2instance() ec2.Instance { + return ec2.Instance{ + InstanceId: inst.id, + InstanceType: inst.instType, + ImageId: inst.imageId, + DNSName: fmt.Sprintf("%s.example.com", inst.id), + // TODO the rest + } +} + +func (inst *Instance) matchAttr(attr, value string) (ok bool, err error) { + switch attr { + case "architecture": + return value == "i386", nil + case "instance-id": + return inst.id == value, nil + case "group-id": + for _, g := range inst.reservation.groups { + if g.id == value { + return true, nil + } + } + return false, nil + case "group-name": + for _, g := range inst.reservation.groups { + if g.name == value { + return true, nil + } + } + return false, nil + case "image-id": + return value == inst.imageId, nil + case "instance-state-code": + code, err := strconv.Atoi(value) + if err != nil { + return false, err + } + return code&0xff == inst.state.Code, nil + case "instance-state-name": + return value == inst.state.Name, nil + } + return false, fmt.Errorf("unknown attribute %q", attr) +} + +var ( + Pending = ec2.InstanceState{0, "pending"} + Running = ec2.InstanceState{16, "running"} + ShuttingDown = ec2.InstanceState{32, "shutting-down"} + Terminated = ec2.InstanceState{16, "terminated"} + Stopped = ec2.InstanceState{16, "stopped"} +) + +func (srv *Server) createSecurityGroup(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + name := req.Form.Get("GroupName") + if name == "" { + fatalf(400, "InvalidParameterValue", "empty security group name") + } + srv.mu.Lock() + defer srv.mu.Unlock() + if srv.group(ec2.SecurityGroup{Name: name}) != nil { + fatalf(400, "InvalidGroup.Duplicate", "group %q already exists", name) + } + g := &securityGroup{ + name: name, + description: req.Form.Get("GroupDescription"), + id: fmt.Sprintf("sg-%d", srv.groupId.next()), + perms: make(map[permKey]bool), + } + srv.groups[g.id] = g + // we define a local type for this because ec2.CreateSecurityGroupResp + // contains SecurityGroup, but the response to this request + // should not contain the security group name. + type CreateSecurityGroupResponse struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` + GroupId string `xml:"groupId"` + } + r := &CreateSecurityGroupResponse{ + RequestId: reqId, + Return: true, + GroupId: g.id, + } + return r +} + +func (srv *Server) notImplemented(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + fatalf(500, "InternalError", "not implemented") + panic("not reached") +} + +func (srv *Server) describeInstances(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + srv.mu.Lock() + defer srv.mu.Unlock() + insts := make(map[*Instance]bool) + for name, vals := range req.Form { + if !strings.HasPrefix(name, "InstanceId.") { + continue + } + inst := srv.instances[vals[0]] + if inst == nil { + fatalf(400, "InvalidInstanceID.NotFound", "instance %q not found", vals[0]) + } + insts[inst] = true + } + + f := newFilter(req.Form) + + var resp ec2.InstancesResp + resp.RequestId = reqId + for _, r := range srv.reservations { + var instances []ec2.Instance + for _, inst := range r.instances { + if len(insts) > 0 && !insts[inst] { + continue + } + ok, err := f.ok(inst) + if ok { + instances = append(instances, inst.ec2instance()) + } else if err != nil { + fatalf(400, "InvalidParameterValue", "describe instances: %v", err) + } + } + if len(instances) > 0 { + var groups []ec2.SecurityGroup + for _, g := range r.groups { + groups = append(groups, g.ec2SecurityGroup()) + } + resp.Reservations = append(resp.Reservations, ec2.Reservation{ + ReservationId: r.id, + OwnerId: ownerId, + Instances: instances, + SecurityGroups: groups, + }) + } + } + return &resp +} + +func (srv *Server) describeSecurityGroups(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + // BUG similar bug to describeInstances, but for GroupName and GroupId + srv.mu.Lock() + defer srv.mu.Unlock() + + var groups []*securityGroup + for name, vals := range req.Form { + var g ec2.SecurityGroup + switch { + case strings.HasPrefix(name, "GroupName."): + g.Name = vals[0] + case strings.HasPrefix(name, "GroupId."): + g.Id = vals[0] + default: + continue + } + sg := srv.group(g) + if sg == nil { + fatalf(400, "InvalidGroup.NotFound", "no such group %v", g) + } + groups = append(groups, sg) + } + if len(groups) == 0 { + for _, g := range srv.groups { + groups = append(groups, g) + } + } + + f := newFilter(req.Form) + var resp ec2.SecurityGroupsResp + resp.RequestId = reqId + for _, group := range groups { + ok, err := f.ok(group) + if ok { + resp.Groups = append(resp.Groups, ec2.SecurityGroupInfo{ + OwnerId: ownerId, + SecurityGroup: group.ec2SecurityGroup(), + Description: group.description, + IPPerms: group.ec2Perms(), + }) + } else if err != nil { + fatalf(400, "InvalidParameterValue", "describe security groups: %v", err) + } + } + return &resp +} + +func (srv *Server) authorizeSecurityGroupIngress(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + srv.mu.Lock() + defer srv.mu.Unlock() + g := srv.group(ec2.SecurityGroup{ + Name: req.Form.Get("GroupName"), + Id: req.Form.Get("GroupId"), + }) + if g == nil { + fatalf(400, "InvalidGroup.NotFound", "group not found") + } + perms := srv.parsePerms(req) + + for _, p := range perms { + if g.perms[p] { + fatalf(400, "InvalidPermission.Duplicate", "Permission has already been authorized on the specified group") + } + } + for _, p := range perms { + g.perms[p] = true + } + return &ec2.SimpleResp{ + XMLName: xml.Name{"", "AuthorizeSecurityGroupIngressResponse"}, + RequestId: reqId, + } +} + +func (srv *Server) revokeSecurityGroupIngress(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + srv.mu.Lock() + defer srv.mu.Unlock() + g := srv.group(ec2.SecurityGroup{ + Name: req.Form.Get("GroupName"), + Id: req.Form.Get("GroupId"), + }) + if g == nil { + fatalf(400, "InvalidGroup.NotFound", "group not found") + } + perms := srv.parsePerms(req) + + // Note EC2 does not give an error if asked to revoke an authorization + // that does not exist. + for _, p := range perms { + delete(g.perms, p) + } + return &ec2.SimpleResp{ + XMLName: xml.Name{"", "RevokeSecurityGroupIngressResponse"}, + RequestId: reqId, + } +} + +var secGroupPat = regexp.MustCompile(`^sg-[a-z0-9]+$`) +var ipPat = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$`) +var ownerIdPat = regexp.MustCompile(`^[0-9]+$`) + +// parsePerms returns a slice of permKey values extracted +// from the permission fields in req. +func (srv *Server) parsePerms(req *http.Request) []permKey { + // perms maps an index found in the form to its associated + // IPPerm. For instance, the form value with key + // "IpPermissions.3.FromPort" will be stored in perms[3].FromPort + perms := make(map[int]ec2.IPPerm) + + type subgroupKey struct { + id1, id2 int + } + // Each IPPerm can have many source security groups. The form key + // for a source security group contains two indices: the index + // of the IPPerm and the sub-index of the security group. The + // sourceGroups map maps from a subgroupKey containing these + // two indices to the associated security group. For instance, + // the form value with key "IPPermissions.3.Groups.2.GroupName" + // will be stored in sourceGroups[subgroupKey{3, 2}].Name. + sourceGroups := make(map[subgroupKey]ec2.UserSecurityGroup) + + // For each value in the form we store its associated information in the + // above maps. The maps are necessary because the form keys may + // arrive in any order, and the indices are not + // necessarily sequential or even small. + for name, vals := range req.Form { + val := vals[0] + var id1 int + var rest string + if x, _ := fmt.Sscanf(name, "IpPermissions.%d.%s", &id1, &rest); x != 2 { + continue + } + ec2p := perms[id1] + switch { + case rest == "FromPort": + ec2p.FromPort = atoi(val) + case rest == "ToPort": + ec2p.ToPort = atoi(val) + case rest == "IpProtocol": + switch val { + case "tcp", "udp", "icmp": + ec2p.Protocol = val + default: + // check it's a well formed number + atoi(val) + ec2p.Protocol = val + } + case strings.HasPrefix(rest, "Groups."): + k := subgroupKey{id1: id1} + if x, _ := fmt.Sscanf(rest[len("Groups."):], "%d.%s", &k.id2, &rest); x != 2 { + continue + } + g := sourceGroups[k] + switch rest { + case "UserId": + // BUG if the user id is blank, this does not conform to the + // way that EC2 handles it - a specified but blank owner id + // can cause RevokeSecurityGroupIngress to fail with + // "group not found" even if the security group id has been + // correctly specified. + // By failing here, we ensure that we fail early in this case. + if !ownerIdPat.MatchString(val) { + fatalf(400, "InvalidUserID.Malformed", "Invalid user ID: %q", val) + } + g.OwnerId = val + case "GroupName": + g.Name = val + case "GroupId": + if !secGroupPat.MatchString(val) { + fatalf(400, "InvalidGroupId.Malformed", "Invalid group ID: %q", val) + } + g.Id = val + default: + fatalf(400, "UnknownParameter", "unknown parameter %q", name) + } + sourceGroups[k] = g + case strings.HasPrefix(rest, "IpRanges."): + var id2 int + if x, _ := fmt.Sscanf(rest[len("IpRanges."):], "%d.%s", &id2, &rest); x != 2 { + continue + } + switch rest { + case "CidrIp": + if !ipPat.MatchString(val) { + fatalf(400, "InvalidPermission.Malformed", "Invalid IP range: %q", val) + } + ec2p.SourceIPs = append(ec2p.SourceIPs, val) + default: + fatalf(400, "UnknownParameter", "unknown parameter %q", name) + } + default: + fatalf(400, "UnknownParameter", "unknown parameter %q", name) + } + perms[id1] = ec2p + } + // Associate each set of source groups with its IPPerm. + for k, g := range sourceGroups { + p := perms[k.id1] + p.SourceGroups = append(p.SourceGroups, g) + perms[k.id1] = p + } + + // Now that we have built up the IPPerms we need, we check for + // parameter errors and build up a permKey for each permission, + // looking up security groups from srv as we do so. + var result []permKey + for _, p := range perms { + if p.FromPort > p.ToPort { + fatalf(400, "InvalidParameterValue", "invalid port range") + } + k := permKey{ + protocol: p.Protocol, + fromPort: p.FromPort, + toPort: p.ToPort, + } + for _, g := range p.SourceGroups { + if g.OwnerId != "" && g.OwnerId != ownerId { + fatalf(400, "InvalidGroup.NotFound", "group %q not found", g.Name) + } + var ec2g ec2.SecurityGroup + switch { + case g.Id != "": + ec2g.Id = g.Id + case g.Name != "": + ec2g.Name = g.Name + } + k.group = srv.group(ec2g) + if k.group == nil { + fatalf(400, "InvalidGroup.NotFound", "group %v not found", g) + } + result = append(result, k) + } + k.group = nil + for _, ip := range p.SourceIPs { + k.ipAddr = ip + result = append(result, k) + } + } + return result +} + +func (srv *Server) deleteSecurityGroup(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + srv.mu.Lock() + defer srv.mu.Unlock() + g := srv.group(ec2.SecurityGroup{ + Name: req.Form.Get("GroupName"), + Id: req.Form.Get("GroupId"), + }) + if g == nil { + fatalf(400, "InvalidGroup.NotFound", "group not found") + } + for _, r := range srv.reservations { + for _, h := range r.groups { + if h == g && r.hasRunningMachine() { + fatalf(500, "InvalidGroup.InUse", "group is currently in use by a running instance") + } + } + } + for _, sg := range srv.groups { + // If a group refers to itself, it's ok to delete it. + if sg == g { + continue + } + for k := range sg.perms { + if k.group == g { + fatalf(500, "InvalidGroup.InUse", "group is currently in use by group %q", sg.id) + } + } + } + + delete(srv.groups, g.id) + return &ec2.SimpleResp{ + XMLName: xml.Name{"", "DeleteSecurityGroupResponse"}, + RequestId: reqId, + } +} + +func (r *reservation) hasRunningMachine() bool { + for _, inst := range r.instances { + if inst.state.Code != ShuttingDown.Code && inst.state.Code != Terminated.Code { + return true + } + } + return false +} + +type counter int + +func (c *counter) next() (i int) { + i = int(*c) + (*c)++ + return +} + +// atoi is like strconv.Atoi but is fatal if the +// string is not well formed. +func atoi(s string) int { + i, err := strconv.Atoi(s) + if err != nil { + fatalf(400, "InvalidParameterValue", "bad number: %v", err) + } + return i +} + +func fatalf(statusCode int, code string, f string, a ...interface{}) { + panic(&ec2.Error{ + StatusCode: statusCode, + Code: code, + Message: fmt.Sprintf(f, a...), + }) +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/export_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/export_test.go new file mode 100644 index 000000000000..1c24422129b4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/export_test.go @@ -0,0 +1,22 @@ +package ec2 + +import ( + "github.com/mitchellh/goamz/aws" + "time" +) + +func Sign(auth aws.Auth, method, path string, params map[string]string, host string) { + sign(auth, method, path, params, host) +} + +func fixedTime() time.Time { + return time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC) +} + +func FakeTime(fakeIt bool) { + if fakeIt { + timeNow = fixedTime + } else { + timeNow = time.Now + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/responses_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/responses_test.go new file mode 100644 index 000000000000..94a681c72fd8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/responses_test.go @@ -0,0 +1,1207 @@ +package ec2_test + +var ErrorDump = ` + +UnsupportedOperation +AMIs with an instance-store root device are not supported for the instance type 't1.micro'. +0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4 +` + +// http://goo.gl/Mcm3b +var RunInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + r-47a5402e + 999988887777 + + + sg-67ad940e + default + + + + + i-2ba64342 + ami-60a54009 + + 0 + pending + + + + example-key-name + 0 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + paravirtual + + + xen + + + i-2bc64242 + ami-60a54009 + + 0 + pending + + + + example-key-name + 1 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + paravirtual + + + xen + + + i-2be64332 + ami-60a54009 + + 0 + pending + + + + example-key-name + 2 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + paravirtual + + + xen + + + +` + +// http://goo.gl/GRZgCD +var RequestSpotInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + sir-1a2b3c4d + 0.5 + one-time + open + + pending-evaluation + 2008-05-07T12:51:50.000Z + Your Spot request has been submitted for review, and is pending evaluation. + + MyAzGroup + + ami-1a2b3c4d + gsg-keypair + + + sg-1a2b3c4d + websrv + + + m1.small + + + false + + false + + YYYY-MM-DDTHH:MM:SS.000Z + Linux/UNIX + + + +` + +// http://goo.gl/KsKJJk +var DescribeSpotRequestsExample = ` + + b1719f2a-5334-4479-b2f1-26926EXAMPLE + + + sir-1a2b3c4d + 0.5 + one-time + active + + fulfilled + 2008-05-07T12:51:50.000Z + Your Spot request is fulfilled. + + + ami-1a2b3c4d + gsg-keypair + + + sg-1a2b3c4d + websrv + + + m1.small + + false + + false + + i-1a2b3c4d + YYYY-MM-DDTHH:MM:SS.000Z + Linux/UNIX + us-east-1a + + + +` + +// http://goo.gl/DcfFgJ +var CancelSpotRequestsExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + sir-1a2b3c4d + cancelled + + + +` + +// http://goo.gl/3BKHj +var TerminateInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + i-3ea74257 + + 32 + shutting-down + + + 16 + running + + + + +` + +// http://goo.gl/mLbmw +var DescribeInstancesExample1 = ` + + 98e3c9a4-848c-4d6d-8e8a-b1bdEXAMPLE + + + r-b27e30d9 + 999988887777 + + + sg-67ad940e + default + + + + + i-c5cd56af + ami-1a2b3c4d + + 16 + running + + domU-12-31-39-10-56-34.compute-1.internal + ec2-174-129-165-232.compute-1.amazonaws.com + + GSG_Keypair + 0 + + m1.small + 2010-08-17T01:15:18.000Z + + us-east-1b + + + aki-94c527fd + ari-96c527ff + + disabled + + 10.198.85.190 + 174.129.165.232 + i386 + ebs + /dev/sda1 + + + /dev/sda1 + + vol-a082c1c9 + attached + 2010-08-17T01:15:21.000Z + false + + + + spot + sir-7a688402 + paravirtual + + + xen + + + 854251627541 + + + r-b67e30dd + 999988887777 + + + sg-67ad940e + default + + + + + i-d9cd56b3 + ami-1a2b3c4d + + 16 + running + + domU-12-31-39-10-54-E5.compute-1.internal + ec2-184-73-58-78.compute-1.amazonaws.com + + GSG_Keypair + 0 + + m1.large + 2010-08-17T01:15:19.000Z + + us-east-1b + + + aki-94c527fd + ari-96c527ff + + disabled + + 10.198.87.19 + 184.73.58.78 + i386 + ebs + /dev/sda1 + + + /dev/sda1 + + vol-a282c1cb + attached + 2010-08-17T01:15:23.000Z + false + + + + spot + sir-55a3aa02 + paravirtual + + + xen + + + 854251627541 + + + +` + +// http://goo.gl/mLbmw +var DescribeInstancesExample2 = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + r-bc7e30d7 + 999988887777 + + + sg-67ad940e + default + + + + + i-c7cd56ad + ami-b232d0db + + 16 + running + + domU-12-31-39-01-76-06.compute-1.internal + ec2-72-44-52-124.compute-1.amazonaws.com + GSG_Keypair + 0 + + m1.small + 2010-08-17T01:15:16.000Z + + us-east-1b + + aki-94c527fd + ari-96c527ff + + disabled + + 10.255.121.240 + 72.44.52.124 + i386 + ebs + /dev/sda1 + + + /dev/sda1 + + vol-a482c1cd + attached + 2010-08-17T01:15:26.000Z + true + + + + paravirtual + + + + webserver + + + + stack + Production + + + xen + + + + + +` + +// http://goo.gl/cxU41 +var CreateImageExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + ami-4fa54026 + +` + +// http://goo.gl/V0U25 +var DescribeImagesExample = ` + + 4a4a27a2-2e7c-475d-b35b-ca822EXAMPLE + + + ami-a2469acf + aws-marketplace/example-marketplace-amzn-ami.1 + available + 123456789999 + true + + + a1b2c3d4e5f6g7h8i9j10k11 + marketplace + + + i386 + machine + aki-805ea7e9 + aws-marketplace + example-marketplace-amzn-ami.1 + Amazon Linux AMI i386 EBS + ebs + /dev/sda1 + + + /dev/sda1 + + snap-787e9403 + 8 + true + + + + paravirtual + xen + + + +` + +// http://goo.gl/bHO3z +var ImageAttributeExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + ami-61a54008 + + + all + + + 495219933132 + + + +` + +// http://goo.gl/ttcda +var CreateSnapshotExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + snap-78a54011 + vol-4d826724 + pending + 2008-05-07T12:51:50.000Z + 60% + 111122223333 + 10 + Daily Backup + +` + +// http://goo.gl/vwU1y +var DeleteSnapshotExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/nkovs +var DescribeSnapshotsExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + snap-1a2b3c4d + vol-8875daef + pending + 2010-07-29T04:12:01.000Z + 30% + 111122223333 + 15 + Daily Backup + + + Purpose + demo_db_14_backup + + + + + +` + +// http://goo.gl/YUjO4G +var ModifyImageAttributeExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/hQwPCK +var CopyImageExample = ` + + 60bc441d-fa2c-494d-b155-5d6a3EXAMPLE + ami-4d3c2b1a + +` + +var CreateKeyPairExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + foo + + 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 + + ---- BEGIN RSA PRIVATE KEY ---- +MIICiTCCAfICCQD6m7oRw0uXOjANBgkqhkiG9w0BAQUFADCBiDELMAkGA1UEBhMC +VVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6 +b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAd +BgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wHhcNMTEwNDI1MjA0NTIxWhcN +MTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYD +VQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25z +b2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFt +YXpvbi5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMaK0dn+a4GmWIWJ +21uUSfwfEvySWtC2XADZ4nB+BLYgVIk60CpiwsZ3G93vUEIO3IyNoH/f0wYK8m9T +rDHudUZg3qX4waLG5M43q7Wgc/MbQITxOUSQv7c7ugFFDzQGBzZswY6786m86gpE +Ibb3OhjZnzcvQAaRHhdlQWIMm2nrAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAtCu4 +nUhVVxYUntneD9+h8Mg9q6q+auNKyExzyLwaxlAoo7TJHidbtS4J5iNmZgXL0Fkb +FFBjvSfpJIlJ00zbhNYS5f6GuoEDmFJl0ZxBHjJnyp378OD8uTs7fLvjx79LjSTb +NYiytVbZPQUQ5Yaxu2jXnimvw3rrszlaEXAMPLE= +-----END RSA PRIVATE KEY----- + + +` + +var DeleteKeyPairExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/Eo7Yl +var CreateSecurityGroupExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + sg-67ad940e + +` + +// http://goo.gl/k12Uy +var DescribeSecurityGroupsExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + 999988887777 + WebServers + sg-67ad940e + Web Servers + + + tcp + 80 + 80 + + + + 0.0.0.0/0 + + + + + + + 999988887777 + RangedPortsBySource + sg-76abc467 + Group A + + + tcp + 6000 + 7000 + + + + + + + +` + +// A dump which includes groups within ip permissions. +var DescribeSecurityGroupsDump = ` + + + 87b92b57-cc6e-48b2-943f-f6f0e5c9f46c + + + 12345 + default + default group + + + icmp + -1 + -1 + + + 12345 + default + sg-67ad940e + + + + + + tcp + 0 + 65535 + + + 12345 + other + sg-76abc467 + + + + + + + + +` + +// http://goo.gl/QJJDO +var DeleteSecurityGroupExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/u2sDJ +var AuthorizeSecurityGroupIngressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/u2sDJ +var AuthorizeSecurityGroupEgressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/Mz7xr +var RevokeSecurityGroupIngressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/Vmkqc +var CreateTagsExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/awKeF +var StartInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + i-10a64379 + + 0 + pending + + + 80 + stopped + + + + +` + +// http://goo.gl/436dJ +var StopInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + i-10a64379 + + 64 + stopping + + + 16 + running + + + + +` + +// http://goo.gl/baoUf +var RebootInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/9rprDN +var AllocateAddressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + 198.51.100.1 + vpc + eipalloc-5723d13e + +` + +// http://goo.gl/DFySJY +var DescribeInstanceStatusExample = ` + + 3be1508e-c444-4fef-89cc-0b1223c4f02fEXAMPLE + + + i-1a2b3c4d + us-east-1d + + 16 + running + + + impaired +
+ + reachability + failed + YYYY-MM-DDTHH:MM:SS.000Z + +
+
+ + impaired +
+ + reachability + failed + YYYY-MM-DDTHH:MM:SS.000Z + +
+
+ + + instance-retirement + The instance is running on degraded hardware + YYYY-MM-DDTHH:MM:SS+0000 + YYYY-MM-DDTHH:MM:SS+0000 + + +
+ + i-2a2b3c4d + us-east-1d + + 16 + running + + + ok +
+ + reachability + passed + +
+
+ + ok +
+ + reachability + passed + +
+
+ + + instance-reboot + The instance is scheduled for a reboot + YYYY-MM-DDTHH:MM:SS+0000 + YYYY-MM-DDTHH:MM:SS+0000 + + +
+ + i-3a2b3c4d + us-east-1c + + 16 + running + + + ok +
+ + reachability + passed + +
+
+ + ok +
+ + reachability + passed + +
+
+
+ + i-4a2b3c4d + us-east-1c + + 16 + running + + + ok +
+ + reachability + passed + +
+
+ + insufficient-data +
+ + reachability + insufficient-data + +
+
+
+
+
+` + +// http://goo.gl/3Q0oCc +var ReleaseAddressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/uOSQE +var AssociateAddressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + eipassoc-fc5ca095 + +` + +// http://goo.gl/LrOa0 +var DisassociateAddressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/icuXh5 +var ModifyInstanceExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +var CreateVpcExample = ` + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + vpc-1a2b3c4d + pending + 10.0.0.0/16 + dopt-1a2b3c4d2 + default + + + +` + +var DescribeVpcsExample = ` + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + + vpc-1a2b3c4d + available + 10.0.0.0/23 + dopt-7a8b9c2d + default + false + + + + +` + +var CreateSubnetExample = ` + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + subnet-9d4a7b6c + pending + vpc-1a2b3c4d + 10.0.1.0/24 + 251 + us-east-1a + + + +` + +// http://goo.gl/tu2Kxm +var ModifySubnetAttributeExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/r6ZCPm +var ResetImageAttributeExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/ylxT4R +var DescribeAvailabilityZonesExample1 = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + us-east-1a + available + us-east-1 + + + + us-east-1b + available + us-east-1 + + + + us-east-1c + available + us-east-1 + + + + us-east-1d + available + us-east-1 + + + + +` + +// http://goo.gl/ylxT4R +var DescribeAvailabilityZonesExample2 = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + us-east-1a + impaired + us-east-1 + + + + us-east-1b + unavailable + us-east-1 + + us-east-1b is currently down for maintenance. + + + + +` + +// http://goo.gl/sdomyE +var CreateNetworkAclExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + acl-5fb85d36 + vpc-11ad4878 + false + + + 32767 + -1 + deny + true + 0.0.0.0/0 + + + 32767 + -1 + deny + false + 0.0.0.0/0 + + + + + + +` + +// http://goo.gl/6sYloC +var CreateNetworkAclEntryRespExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/5tqceF +var DescribeNetworkAclsExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + acl-5566953c + vpc-5266953b + true + + + 100 + -1 + allow + true + 0.0.0.0/0 + + + 32767 + -1 + deny + true + 0.0.0.0/0 + + + 100 + -1 + allow + false + 0.0.0.0/0 + + + 32767 + -1 + deny + false + 0.0.0.0/0 + + + + + + + acl-5d659634 + vpc-5266953b + false + + + 110 + 6 + allow + true + 0.0.0.0/0 + + 49152 + 65535 + + + + 32767 + -1 + deny + true + 0.0.0.0/0 + + + 110 + 6 + allow + false + 0.0.0.0/0 + + 80 + 80 + + + + 120 + 6 + allow + false + 0.0.0.0/0 + + 443 + 443 + + + + 32767 + -1 + deny + false + 0.0.0.0/0 + + + + + aclassoc-5c659635 + acl-5d659634 + subnet-ff669596 + + + aclassoc-c26596ab + acl-5d659634 + subnet-f0669599 + + + + + + +` + +var ReplaceNetworkAclAssociationResponseExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + aclassoc-17b85d7e + +` diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign.go new file mode 100644 index 000000000000..bffc3c7e9303 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign.go @@ -0,0 +1,45 @@ +package ec2 + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "github.com/mitchellh/goamz/aws" + "sort" + "strings" +) + +// ---------------------------------------------------------------------------- +// EC2 signing (http://goo.gl/fQmAN) + +var b64 = base64.StdEncoding + +func sign(auth aws.Auth, method, path string, params map[string]string, host string) { + params["AWSAccessKeyId"] = auth.AccessKey + params["SignatureVersion"] = "2" + params["SignatureMethod"] = "HmacSHA256" + if auth.Token != "" { + params["SecurityToken"] = auth.Token + } + + // AWS specifies that the parameters in a signed request must + // be provided in the natural order of the keys. This is distinct + // from the natural order of the encoded value of key=value. + // Percent and equals affect the sorting order. + var keys, sarray []string + for k, _ := range params { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + sarray = append(sarray, aws.Encode(k)+"="+aws.Encode(params[k])) + } + joined := strings.Join(sarray, "&") + payload := method + "\n" + host + "\n" + path + "\n" + joined + hash := hmac.New(sha256.New, []byte(auth.SecretKey)) + hash.Write([]byte(payload)) + signature := make([]byte, b64.EncodedLen(hash.Size())) + b64.Encode(signature, hash.Sum(nil)) + + params["Signature"] = string(signature) +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign_test.go new file mode 100644 index 000000000000..86d203e78c63 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign_test.go @@ -0,0 +1,68 @@ +package ec2_test + +import ( + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + . "github.com/motain/gocheck" +) + +// EC2 ReST authentication docs: http://goo.gl/fQmAN + +var testAuth = aws.Auth{"user", "secret", ""} + +func (s *S) TestBasicSignature(c *C) { + params := map[string]string{} + ec2.Sign(testAuth, "GET", "/path", params, "localhost") + c.Assert(params["SignatureVersion"], Equals, "2") + c.Assert(params["SignatureMethod"], Equals, "HmacSHA256") + expected := "6lSe5QyXum0jMVc7cOUz32/52ZnL7N5RyKRk/09yiK4=" + c.Assert(params["Signature"], Equals, expected) +} + +func (s *S) TestParamSignature(c *C) { + params := map[string]string{ + "param1": "value1", + "param2": "value2", + "param3": "value3", + } + ec2.Sign(testAuth, "GET", "/path", params, "localhost") + expected := "XWOR4+0lmK8bD8CGDGZ4kfuSPbb2JibLJiCl/OPu1oU=" + c.Assert(params["Signature"], Equals, expected) +} + +func (s *S) TestManyParams(c *C) { + params := map[string]string{ + "param1": "value10", + "param2": "value2", + "param3": "value3", + "param4": "value4", + "param5": "value5", + "param6": "value6", + "param7": "value7", + "param8": "value8", + "param9": "value9", + "param10": "value1", + } + ec2.Sign(testAuth, "GET", "/path", params, "localhost") + expected := "di0sjxIvezUgQ1SIL6i+C/H8lL+U0CQ9frLIak8jkVg=" + c.Assert(params["Signature"], Equals, expected) +} + +func (s *S) TestEscaping(c *C) { + params := map[string]string{"Nonce": "+ +"} + ec2.Sign(testAuth, "GET", "/path", params, "localhost") + c.Assert(params["Nonce"], Equals, "+ +") + expected := "bqffDELReIqwjg/W0DnsnVUmfLK4wXVLO4/LuG+1VFA=" + c.Assert(params["Signature"], Equals, expected) +} + +func (s *S) TestSignatureExample1(c *C) { + params := map[string]string{ + "Timestamp": "2009-02-01T12:53:20+00:00", + "Version": "2007-11-07", + "Action": "ListDomains", + } + ec2.Sign(aws.Auth{"access", "secret", ""}, "GET", "/", params, "sdb.amazonaws.com") + expected := "okj96/5ucWBSc1uR2zXVfm6mDHtgfNv657rRtt/aunQ=" + c.Assert(params["Signature"], Equals, expected) +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/LICENSE b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/LICENSE new file mode 100644 index 000000000000..f9c841a51e0d --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/README.md b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/README.md new file mode 100644 index 000000000000..659d6885fc7e --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/README.md @@ -0,0 +1,46 @@ +# mapstructure + +mapstructure is a Go library for decoding generic map values to structures +and vice versa, while providing helpful error handling. + +This library is most useful when decoding values from some data stream (JSON, +Gob, etc.) where you don't _quite_ know the structure of the underlying data +until you read a part of it. You can therefore read a `map[string]interface{}` +and use this library to decode it into the proper underlying native Go +structure. + +## Installation + +Standard `go get`: + +``` +$ go get github.com/mitchellh/mapstructure +``` + +## Usage & Example + +For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/mapstructure). + +The `Decode` function has examples associated with it there. + +## But Why?! + +Go offers fantastic standard libraries for decoding formats such as JSON. +The standard method is to have a struct pre-created, and populate that struct +from the bytes of the encoded format. This is great, but the problem is if +you have configuration or an encoding that changes slightly depending on +specific fields. For example, consider this JSON: + +```json +{ + "type": "person", + "name": "Mitchell" +} +``` + +Perhaps we can't populate a specific structure without first reading +the "type" field from the JSON. We could always do two passes over the +decoding of the JSON (reading the "type" first, and the rest later). +However, it is much simpler to just decode this into a `map[string]interface{}` +structure, read the "type" key, then use something like this library +to decode it into the proper structure. diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks.go new file mode 100644 index 000000000000..087a392b917e --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks.go @@ -0,0 +1,84 @@ +package mapstructure + +import ( + "reflect" + "strconv" + "strings" +) + +// ComposeDecodeHookFunc creates a single DecodeHookFunc that +// automatically composes multiple DecodeHookFuncs. +// +// The composed funcs are called in order, with the result of the +// previous transformation. +func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc { + return func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + var err error + for _, f1 := range fs { + data, err = f1(f, t, data) + if err != nil { + return nil, err + } + + // Modify the from kind to be correct with the new data + f = getKind(reflect.ValueOf(data)) + } + + return data, nil + } +} + +// StringToSliceHookFunc returns a DecodeHookFunc that converts +// string to []string by splitting on the given sep. +func StringToSliceHookFunc(sep string) DecodeHookFunc { + return func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + if f != reflect.String || t != reflect.Slice { + return data, nil + } + + raw := data.(string) + if raw == "" { + return []string{}, nil + } + + return strings.Split(raw, sep), nil + } +} + +func WeaklyTypedHook( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + dataVal := reflect.ValueOf(data) + switch t { + case reflect.String: + switch f { + case reflect.Bool: + if dataVal.Bool() { + return "1", nil + } else { + return "0", nil + } + case reflect.Float32: + return strconv.FormatFloat(dataVal.Float(), 'f', -1, 64), nil + case reflect.Int: + return strconv.FormatInt(dataVal.Int(), 10), nil + case reflect.Slice: + dataType := dataVal.Type() + elemKind := dataType.Elem().Kind() + if elemKind == reflect.Uint8 { + return string(dataVal.Interface().([]uint8)), nil + } + case reflect.Uint: + return strconv.FormatUint(dataVal.Uint(), 10), nil + } + } + + return data, nil +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks_test.go new file mode 100644 index 000000000000..b417deeb64d2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks_test.go @@ -0,0 +1,191 @@ +package mapstructure + +import ( + "errors" + "reflect" + "testing" +) + +func TestComposeDecodeHookFunc(t *testing.T) { + f1 := func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + return data.(string) + "foo", nil + } + + f2 := func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + return data.(string) + "bar", nil + } + + f := ComposeDecodeHookFunc(f1, f2) + + result, err := f(reflect.String, reflect.Slice, "") + if err != nil { + t.Fatalf("bad: %s", err) + } + if result.(string) != "foobar" { + t.Fatalf("bad: %#v", result) + } +} + +func TestComposeDecodeHookFunc_err(t *testing.T) { + f1 := func(reflect.Kind, reflect.Kind, interface{}) (interface{}, error) { + return nil, errors.New("foo") + } + + f2 := func(reflect.Kind, reflect.Kind, interface{}) (interface{}, error) { + panic("NOPE") + } + + f := ComposeDecodeHookFunc(f1, f2) + + _, err := f(reflect.String, reflect.Slice, 42) + if err.Error() != "foo" { + t.Fatalf("bad: %s", err) + } +} + +func TestComposeDecodeHookFunc_kinds(t *testing.T) { + var f2From reflect.Kind + + f1 := func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + return int(42), nil + } + + f2 := func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + f2From = f + return data, nil + } + + f := ComposeDecodeHookFunc(f1, f2) + + _, err := f(reflect.String, reflect.Slice, "") + if err != nil { + t.Fatalf("bad: %s", err) + } + if f2From != reflect.Int { + t.Fatalf("bad: %#v", f2From) + } +} + +func TestStringToSliceHookFunc(t *testing.T) { + f := StringToSliceHookFunc(",") + + cases := []struct { + f, t reflect.Kind + data interface{} + result interface{} + err bool + }{ + {reflect.Slice, reflect.Slice, 42, 42, false}, + {reflect.String, reflect.String, 42, 42, false}, + { + reflect.String, + reflect.Slice, + "foo,bar,baz", + []string{"foo", "bar", "baz"}, + false, + }, + { + reflect.String, + reflect.Slice, + "", + []string{}, + false, + }, + } + + for i, tc := range cases { + actual, err := f(tc.f, tc.t, tc.data) + if tc.err != (err != nil) { + t.Fatalf("case %d: expected err %#v", i, tc.err) + } + if !reflect.DeepEqual(actual, tc.result) { + t.Fatalf( + "case %d: expected %#v, got %#v", + i, tc.result, actual) + } + } +} + +func TestWeaklyTypedHook(t *testing.T) { + var f DecodeHookFunc = WeaklyTypedHook + + cases := []struct { + f, t reflect.Kind + data interface{} + result interface{} + err bool + }{ + // TO STRING + { + reflect.Bool, + reflect.String, + false, + "0", + false, + }, + + { + reflect.Bool, + reflect.String, + true, + "1", + false, + }, + + { + reflect.Float32, + reflect.String, + float32(7), + "7", + false, + }, + + { + reflect.Int, + reflect.String, + int(7), + "7", + false, + }, + + { + reflect.Slice, + reflect.String, + []uint8("foo"), + "foo", + false, + }, + + { + reflect.Uint, + reflect.String, + uint(7), + "7", + false, + }, + } + + for i, tc := range cases { + actual, err := f(tc.f, tc.t, tc.data) + if tc.err != (err != nil) { + t.Fatalf("case %d: expected err %#v", i, tc.err) + } + if !reflect.DeepEqual(actual, tc.result) { + t.Fatalf( + "case %d: expected %#v, got %#v", + i, tc.result, actual) + } + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/error.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/error.go new file mode 100644 index 000000000000..3460799f801f --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/error.go @@ -0,0 +1,32 @@ +package mapstructure + +import ( + "fmt" + "strings" +) + +// Error implements the error interface and can represents multiple +// errors that occur in the course of a single decode. +type Error struct { + Errors []string +} + +func (e *Error) Error() string { + points := make([]string, len(e.Errors)) + for i, err := range e.Errors { + points[i] = fmt.Sprintf("* %s", err) + } + + return fmt.Sprintf( + "%d error(s) decoding:\n\n%s", + len(e.Errors), strings.Join(points, "\n")) +} + +func appendErrors(errors []string, err error) []string { + switch e := err.(type) { + case *Error: + return append(errors, e.Errors...) + default: + return append(errors, e.Error()) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure.go new file mode 100644 index 000000000000..381ba5d48763 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure.go @@ -0,0 +1,704 @@ +// The mapstructure package exposes functionality to convert an +// abitrary map[string]interface{} into a native Go structure. +// +// The Go structure can be arbitrarily complex, containing slices, +// other structs, etc. and the decoder will properly decode nested +// maps and so on into the proper structures in the native Go struct. +// See the examples to see what the decoder is capable of. +package mapstructure + +import ( + "errors" + "fmt" + "reflect" + "sort" + "strconv" + "strings" +) + +// DecodeHookFunc is the callback function that can be used for +// data transformations. See "DecodeHook" in the DecoderConfig +// struct. +type DecodeHookFunc func( + from reflect.Kind, + to reflect.Kind, + data interface{}) (interface{}, error) + +// DecoderConfig is the configuration that is used to create a new decoder +// and allows customization of various aspects of decoding. +type DecoderConfig struct { + // DecodeHook, if set, will be called before any decoding and any + // type conversion (if WeaklyTypedInput is on). This lets you modify + // the values before they're set down onto the resulting struct. + // + // If an error is returned, the entire decode will fail with that + // error. + DecodeHook DecodeHookFunc + + // If ErrorUnused is true, then it is an error for there to exist + // keys in the original map that were unused in the decoding process + // (extra keys). + ErrorUnused bool + + // If WeaklyTypedInput is true, the decoder will make the following + // "weak" conversions: + // + // - bools to string (true = "1", false = "0") + // - numbers to string (base 10) + // - bools to int/uint (true = 1, false = 0) + // - strings to int/uint (base implied by prefix) + // - int to bool (true if value != 0) + // - string to bool (accepts: 1, t, T, TRUE, true, True, 0, f, F, + // FALSE, false, False. Anything else is an error) + // - empty array = empty map and vice versa + // + WeaklyTypedInput bool + + // Metadata is the struct that will contain extra metadata about + // the decoding. If this is nil, then no metadata will be tracked. + Metadata *Metadata + + // Result is a pointer to the struct that will contain the decoded + // value. + Result interface{} + + // The tag name that mapstructure reads for field names. This + // defaults to "mapstructure" + TagName string +} + +// A Decoder takes a raw interface value and turns it into structured +// data, keeping track of rich error information along the way in case +// anything goes wrong. Unlike the basic top-level Decode method, you can +// more finely control how the Decoder behaves using the DecoderConfig +// structure. The top-level Decode method is just a convenience that sets +// up the most basic Decoder. +type Decoder struct { + config *DecoderConfig +} + +// Metadata contains information about decoding a structure that +// is tedious or difficult to get otherwise. +type Metadata struct { + // Keys are the keys of the structure which were successfully decoded + Keys []string + + // Unused is a slice of keys that were found in the raw value but + // weren't decoded since there was no matching field in the result interface + Unused []string +} + +// Decode takes a map and uses reflection to convert it into the +// given Go native structure. val must be a pointer to a struct. +func Decode(m interface{}, rawVal interface{}) error { + config := &DecoderConfig{ + Metadata: nil, + Result: rawVal, + } + + decoder, err := NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(m) +} + +// WeakDecode is the same as Decode but is shorthand to enable +// WeaklyTypedInput. See DecoderConfig for more info. +func WeakDecode(input, output interface{}) error { + config := &DecoderConfig{ + Metadata: nil, + Result: output, + WeaklyTypedInput: true, + } + + decoder, err := NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + +// NewDecoder returns a new decoder for the given configuration. Once +// a decoder has been returned, the same configuration must not be used +// again. +func NewDecoder(config *DecoderConfig) (*Decoder, error) { + val := reflect.ValueOf(config.Result) + if val.Kind() != reflect.Ptr { + return nil, errors.New("result must be a pointer") + } + + val = val.Elem() + if !val.CanAddr() { + return nil, errors.New("result must be addressable (a pointer)") + } + + if config.Metadata != nil { + if config.Metadata.Keys == nil { + config.Metadata.Keys = make([]string, 0) + } + + if config.Metadata.Unused == nil { + config.Metadata.Unused = make([]string, 0) + } + } + + if config.TagName == "" { + config.TagName = "mapstructure" + } + + result := &Decoder{ + config: config, + } + + return result, nil +} + +// Decode decodes the given raw interface to the target pointer specified +// by the configuration. +func (d *Decoder) Decode(raw interface{}) error { + return d.decode("", raw, reflect.ValueOf(d.config.Result).Elem()) +} + +// Decodes an unknown data type into a specific reflection value. +func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error { + if data == nil { + // If the data is nil, then we don't set anything. + return nil + } + + dataVal := reflect.ValueOf(data) + if !dataVal.IsValid() { + // If the data value is invalid, then we just set the value + // to be the zero value. + val.Set(reflect.Zero(val.Type())) + return nil + } + + if d.config.DecodeHook != nil { + // We have a DecodeHook, so let's pre-process the data. + var err error + data, err = d.config.DecodeHook(getKind(dataVal), getKind(val), data) + if err != nil { + return err + } + } + + var err error + dataKind := getKind(val) + switch dataKind { + case reflect.Bool: + err = d.decodeBool(name, data, val) + case reflect.Interface: + err = d.decodeBasic(name, data, val) + case reflect.String: + err = d.decodeString(name, data, val) + case reflect.Int: + err = d.decodeInt(name, data, val) + case reflect.Uint: + err = d.decodeUint(name, data, val) + case reflect.Float32: + err = d.decodeFloat(name, data, val) + case reflect.Struct: + err = d.decodeStruct(name, data, val) + case reflect.Map: + err = d.decodeMap(name, data, val) + case reflect.Ptr: + err = d.decodePtr(name, data, val) + case reflect.Slice: + err = d.decodeSlice(name, data, val) + default: + // If we reached this point then we weren't able to decode it + return fmt.Errorf("%s: unsupported type: %s", name, dataKind) + } + + // If we reached here, then we successfully decoded SOMETHING, so + // mark the key as used if we're tracking metadata. + if d.config.Metadata != nil && name != "" { + d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) + } + + return err +} + +// This decodes a basic type (bool, int, string, etc.) and sets the +// value to "data" of that type. +func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataValType := dataVal.Type() + if !dataValType.AssignableTo(val.Type()) { + return fmt.Errorf( + "'%s' expected type '%s', got '%s'", + name, val.Type(), dataValType) + } + + val.Set(dataVal) + return nil +} + +func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + converted := true + switch { + case dataKind == reflect.String: + val.SetString(dataVal.String()) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetString("1") + } else { + val.SetString("0") + } + case dataKind == reflect.Int && d.config.WeaklyTypedInput: + val.SetString(strconv.FormatInt(dataVal.Int(), 10)) + case dataKind == reflect.Uint && d.config.WeaklyTypedInput: + val.SetString(strconv.FormatUint(dataVal.Uint(), 10)) + case dataKind == reflect.Float32 && d.config.WeaklyTypedInput: + val.SetString(strconv.FormatFloat(dataVal.Float(), 'f', -1, 64)) + case dataKind == reflect.Slice && d.config.WeaklyTypedInput: + dataType := dataVal.Type() + elemKind := dataType.Elem().Kind() + switch { + case elemKind == reflect.Uint8: + val.SetString(string(dataVal.Interface().([]uint8))) + default: + converted = false + } + default: + converted = false + } + + if !converted { + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Int: + val.SetInt(dataVal.Int()) + case dataKind == reflect.Uint: + val.SetInt(int64(dataVal.Uint())) + case dataKind == reflect.Float32: + val.SetInt(int64(dataVal.Float())) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetInt(1) + } else { + val.SetInt(0) + } + case dataKind == reflect.String && d.config.WeaklyTypedInput: + i, err := strconv.ParseInt(dataVal.String(), 0, val.Type().Bits()) + if err == nil { + val.SetInt(i) + } else { + return fmt.Errorf("cannot parse '%s' as int: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Int: + val.SetUint(uint64(dataVal.Int())) + case dataKind == reflect.Uint: + val.SetUint(dataVal.Uint()) + case dataKind == reflect.Float32: + val.SetUint(uint64(dataVal.Float())) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetUint(1) + } else { + val.SetUint(0) + } + case dataKind == reflect.String && d.config.WeaklyTypedInput: + i, err := strconv.ParseUint(dataVal.String(), 0, val.Type().Bits()) + if err == nil { + val.SetUint(i) + } else { + return fmt.Errorf("cannot parse '%s' as uint: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Bool: + val.SetBool(dataVal.Bool()) + case dataKind == reflect.Int && d.config.WeaklyTypedInput: + val.SetBool(dataVal.Int() != 0) + case dataKind == reflect.Uint && d.config.WeaklyTypedInput: + val.SetBool(dataVal.Uint() != 0) + case dataKind == reflect.Float32 && d.config.WeaklyTypedInput: + val.SetBool(dataVal.Float() != 0) + case dataKind == reflect.String && d.config.WeaklyTypedInput: + b, err := strconv.ParseBool(dataVal.String()) + if err == nil { + val.SetBool(b) + } else if dataVal.String() == "" { + val.SetBool(false) + } else { + return fmt.Errorf("cannot parse '%s' as bool: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeFloat(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Int: + val.SetFloat(float64(dataVal.Int())) + case dataKind == reflect.Uint: + val.SetFloat(float64(dataVal.Uint())) + case dataKind == reflect.Float32: + val.SetFloat(float64(dataVal.Float())) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetFloat(1) + } else { + val.SetFloat(0) + } + case dataKind == reflect.String && d.config.WeaklyTypedInput: + f, err := strconv.ParseFloat(dataVal.String(), val.Type().Bits()) + if err == nil { + val.SetFloat(f) + } else { + return fmt.Errorf("cannot parse '%s' as float: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error { + valType := val.Type() + valKeyType := valType.Key() + valElemType := valType.Elem() + + // Make a new map to hold our result + mapType := reflect.MapOf(valKeyType, valElemType) + valMap := reflect.MakeMap(mapType) + + // Check input type + dataVal := reflect.Indirect(reflect.ValueOf(data)) + if dataVal.Kind() != reflect.Map { + // Accept empty array/slice instead of an empty map in weakly typed mode + if d.config.WeaklyTypedInput && + (dataVal.Kind() == reflect.Slice || dataVal.Kind() == reflect.Array) && + dataVal.Len() == 0 { + val.Set(valMap) + return nil + } else { + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) + } + } + + // Accumulate errors + errors := make([]string, 0) + + for _, k := range dataVal.MapKeys() { + fieldName := fmt.Sprintf("%s[%s]", name, k) + + // First decode the key into the proper type + currentKey := reflect.Indirect(reflect.New(valKeyType)) + if err := d.decode(fieldName, k.Interface(), currentKey); err != nil { + errors = appendErrors(errors, err) + continue + } + + // Next decode the data into the proper type + v := dataVal.MapIndex(k).Interface() + currentVal := reflect.Indirect(reflect.New(valElemType)) + if err := d.decode(fieldName, v, currentVal); err != nil { + errors = appendErrors(errors, err) + continue + } + + valMap.SetMapIndex(currentKey, currentVal) + } + + // Set the built up map to the value + val.Set(valMap) + + // If we had errors, return those + if len(errors) > 0 { + return &Error{errors} + } + + return nil +} + +func (d *Decoder) decodePtr(name string, data interface{}, val reflect.Value) error { + // Create an element of the concrete (non pointer) type and decode + // into that. Then set the value of the pointer to this type. + valType := val.Type() + valElemType := valType.Elem() + realVal := reflect.New(valElemType) + if err := d.decode(name, data, reflect.Indirect(realVal)); err != nil { + return err + } + + val.Set(realVal) + return nil +} + +func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + dataValKind := dataVal.Kind() + valType := val.Type() + valElemType := valType.Elem() + sliceType := reflect.SliceOf(valElemType) + + // Check input type + if dataValKind != reflect.Array && dataValKind != reflect.Slice { + // Accept empty map instead of array/slice in weakly typed mode + if d.config.WeaklyTypedInput && dataVal.Kind() == reflect.Map && dataVal.Len() == 0 { + val.Set(reflect.MakeSlice(sliceType, 0, 0)) + return nil + } else { + return fmt.Errorf( + "'%s': source data must be an array or slice, got %s", name, dataValKind) + } + } + + // Make a new slice to hold our result, same size as the original data. + valSlice := reflect.MakeSlice(sliceType, dataVal.Len(), dataVal.Len()) + + // Accumulate any errors + errors := make([]string, 0) + + for i := 0; i < dataVal.Len(); i++ { + currentData := dataVal.Index(i).Interface() + currentField := valSlice.Index(i) + + fieldName := fmt.Sprintf("%s[%d]", name, i) + if err := d.decode(fieldName, currentData, currentField); err != nil { + errors = appendErrors(errors, err) + } + } + + // Finally, set the value to the slice we built up + val.Set(valSlice) + + // If there were errors, we return those + if len(errors) > 0 { + return &Error{errors} + } + + return nil +} + +func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + dataValKind := dataVal.Kind() + if dataValKind != reflect.Map { + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataValKind) + } + + dataValType := dataVal.Type() + if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { + return fmt.Errorf( + "'%s' needs a map with string keys, has '%s' keys", + name, dataValType.Key().Kind()) + } + + dataValKeys := make(map[reflect.Value]struct{}) + dataValKeysUnused := make(map[interface{}]struct{}) + for _, dataValKey := range dataVal.MapKeys() { + dataValKeys[dataValKey] = struct{}{} + dataValKeysUnused[dataValKey.Interface()] = struct{}{} + } + + errors := make([]string, 0) + + // This slice will keep track of all the structs we'll be decoding. + // There can be more than one struct if there are embedded structs + // that are squashed. + structs := make([]reflect.Value, 1, 5) + structs[0] = val + + // Compile the list of all the fields that we're going to be decoding + // from all the structs. + fields := make(map[*reflect.StructField]reflect.Value) + for len(structs) > 0 { + structVal := structs[0] + structs = structs[1:] + + structType := structVal.Type() + for i := 0; i < structType.NumField(); i++ { + fieldType := structType.Field(i) + + if fieldType.Anonymous { + fieldKind := fieldType.Type.Kind() + if fieldKind != reflect.Struct { + errors = appendErrors(errors, + fmt.Errorf("%s: unsupported type: %s", fieldType.Name, fieldKind)) + continue + } + + // We have an embedded field. We "squash" the fields down + // if specified in the tag. + squash := false + tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",") + for _, tag := range tagParts[1:] { + if tag == "squash" { + squash = true + break + } + } + + if squash { + structs = append(structs, val.FieldByName(fieldType.Name)) + continue + } + } + + // Normal struct field, store it away + fields[&fieldType] = structVal.Field(i) + } + } + + for fieldType, field := range fields { + fieldName := fieldType.Name + + tagValue := fieldType.Tag.Get(d.config.TagName) + tagValue = strings.SplitN(tagValue, ",", 2)[0] + if tagValue != "" { + fieldName = tagValue + } + + rawMapKey := reflect.ValueOf(fieldName) + rawMapVal := dataVal.MapIndex(rawMapKey) + if !rawMapVal.IsValid() { + // Do a slower search by iterating over each key and + // doing case-insensitive search. + for dataValKey, _ := range dataValKeys { + mK, ok := dataValKey.Interface().(string) + if !ok { + // Not a string key + continue + } + + if strings.EqualFold(mK, fieldName) { + rawMapKey = dataValKey + rawMapVal = dataVal.MapIndex(dataValKey) + break + } + } + + if !rawMapVal.IsValid() { + // There was no matching key in the map for the value in + // the struct. Just ignore. + continue + } + } + + // Delete the key we're using from the unused map so we stop tracking + delete(dataValKeysUnused, rawMapKey.Interface()) + + if !field.IsValid() { + // This should never happen + panic("field is not valid") + } + + // If we can't set the field, then it is unexported or something, + // and we just continue onwards. + if !field.CanSet() { + continue + } + + // If the name is empty string, then we're at the root, and we + // don't dot-join the fields. + if name != "" { + fieldName = fmt.Sprintf("%s.%s", name, fieldName) + } + + if err := d.decode(fieldName, rawMapVal.Interface(), field); err != nil { + errors = appendErrors(errors, err) + } + } + + if d.config.ErrorUnused && len(dataValKeysUnused) > 0 { + keys := make([]string, 0, len(dataValKeysUnused)) + for rawKey, _ := range dataValKeysUnused { + keys = append(keys, rawKey.(string)) + } + sort.Strings(keys) + + err := fmt.Errorf("'%s' has invalid keys: %s", name, strings.Join(keys, ", ")) + errors = appendErrors(errors, err) + } + + if len(errors) > 0 { + return &Error{errors} + } + + // Add the unused keys to the list of unused keys if we're tracking metadata + if d.config.Metadata != nil { + for rawKey, _ := range dataValKeysUnused { + key := rawKey.(string) + if name != "" { + key = fmt.Sprintf("%s.%s", name, key) + } + + d.config.Metadata.Unused = append(d.config.Metadata.Unused, key) + } + } + + return nil +} + +func getKind(val reflect.Value) reflect.Kind { + kind := val.Kind() + + switch { + case kind >= reflect.Int && kind <= reflect.Int64: + return reflect.Int + case kind >= reflect.Uint && kind <= reflect.Uint64: + return reflect.Uint + case kind >= reflect.Float32 && kind <= reflect.Float64: + return reflect.Float32 + default: + return kind + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_benchmark_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_benchmark_test.go new file mode 100644 index 000000000000..b50ac36e5d06 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_benchmark_test.go @@ -0,0 +1,243 @@ +package mapstructure + +import ( + "testing" +) + +func Benchmark_Decode(b *testing.B) { + type Person struct { + Name string + Age int + Emails []string + Extra map[string]string + } + + input := map[string]interface{}{ + "name": "Mitchell", + "age": 91, + "emails": []string{"one", "two", "three"}, + "extra": map[string]string{ + "twitter": "mitchellh", + }, + } + + var result Person + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeBasic(b *testing.B) { + input := map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "Vuint": 42, + "vbool": true, + "Vfloat": 42.42, + "vsilent": true, + "vdata": 42, + } + + var result Basic + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeEmbedded(b *testing.B) { + input := map[string]interface{}{ + "vstring": "foo", + "Basic": map[string]interface{}{ + "vstring": "innerfoo", + }, + "vunique": "bar", + } + + var result Embedded + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeTypeConversion(b *testing.B) { + input := map[string]interface{}{ + "IntToFloat": 42, + "IntToUint": 42, + "IntToBool": 1, + "IntToString": 42, + "UintToInt": 42, + "UintToFloat": 42, + "UintToBool": 42, + "UintToString": 42, + "BoolToInt": true, + "BoolToUint": true, + "BoolToFloat": true, + "BoolToString": true, + "FloatToInt": 42.42, + "FloatToUint": 42.42, + "FloatToBool": 42.42, + "FloatToString": 42.42, + "StringToInt": "42", + "StringToUint": "42", + "StringToBool": "1", + "StringToFloat": "42.42", + "SliceToMap": []interface{}{}, + "MapToSlice": map[string]interface{}{}, + } + + var resultStrict TypeConversionResult + for i := 0; i < b.N; i++ { + Decode(input, &resultStrict) + } +} + +func Benchmark_DecodeMap(b *testing.B) { + input := map[string]interface{}{ + "vfoo": "foo", + "vother": map[interface{}]interface{}{ + "foo": "foo", + "bar": "bar", + }, + } + + var result Map + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeMapOfStruct(b *testing.B) { + input := map[string]interface{}{ + "value": map[string]interface{}{ + "foo": map[string]string{"vstring": "one"}, + "bar": map[string]string{"vstring": "two"}, + }, + } + + var result MapOfStruct + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeSlice(b *testing.B) { + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": []string{"foo", "bar", "baz"}, + } + + var result Slice + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeSliceOfStruct(b *testing.B) { + input := map[string]interface{}{ + "value": []map[string]interface{}{ + {"vstring": "one"}, + {"vstring": "two"}, + }, + } + + var result SliceOfStruct + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeWeaklyTypedInput(b *testing.B) { + type Person struct { + Name string + Age int + Emails []string + } + + // This input can come from anywhere, but typically comes from + // something like decoding JSON, generated by a weakly typed language + // such as PHP. + input := map[string]interface{}{ + "name": 123, // number => string + "age": "42", // string => number + "emails": map[string]interface{}{}, // empty map => empty array + } + + var result Person + config := &DecoderConfig{ + WeaklyTypedInput: true, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + panic(err) + } + + for i := 0; i < b.N; i++ { + decoder.Decode(input) + } +} + +func Benchmark_DecodeMetadata(b *testing.B) { + type Person struct { + Name string + Age int + } + + input := map[string]interface{}{ + "name": "Mitchell", + "age": 91, + "email": "foo@bar.com", + } + + var md Metadata + var result Person + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + panic(err) + } + + for i := 0; i < b.N; i++ { + decoder.Decode(input) + } +} + +func Benchmark_DecodeMetadataEmbedded(b *testing.B) { + input := map[string]interface{}{ + "vstring": "foo", + "vunique": "bar", + } + + var md Metadata + var result EmbeddedSquash + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + b.Fatalf("err: %s", err) + } + + for i := 0; i < b.N; i++ { + decoder.Decode(input) + } +} + +func Benchmark_DecodeTagged(b *testing.B) { + input := map[string]interface{}{ + "foo": "bar", + "bar": "value", + } + + var result Tagged + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_bugs_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_bugs_test.go new file mode 100644 index 000000000000..7054f1ac9abc --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_bugs_test.go @@ -0,0 +1,47 @@ +package mapstructure + +import "testing" + +// GH-1 +func TestDecode_NilValue(t *testing.T) { + input := map[string]interface{}{ + "vfoo": nil, + "vother": nil, + } + + var result Map + err := Decode(input, &result) + if err != nil { + t.Fatalf("should not error: %s", err) + } + + if result.Vfoo != "" { + t.Fatalf("value should be default: %s", result.Vfoo) + } + + if result.Vother != nil { + t.Fatalf("Vother should be nil: %s", result.Vother) + } +} + +// GH-10 +func TestDecode_mapInterfaceInterface(t *testing.T) { + input := map[interface{}]interface{}{ + "vfoo": nil, + "vother": nil, + } + + var result Map + err := Decode(input, &result) + if err != nil { + t.Fatalf("should not error: %s", err) + } + + if result.Vfoo != "" { + t.Fatalf("value should be default: %s", result.Vfoo) + } + + if result.Vother != nil { + t.Fatalf("Vother should be nil: %s", result.Vother) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_examples_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_examples_test.go new file mode 100644 index 000000000000..aa393cc5721f --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_examples_test.go @@ -0,0 +1,169 @@ +package mapstructure + +import ( + "fmt" +) + +func ExampleDecode() { + type Person struct { + Name string + Age int + Emails []string + Extra map[string]string + } + + // This input can come from anywhere, but typically comes from + // something like decoding JSON where we're not quite sure of the + // struct initially. + input := map[string]interface{}{ + "name": "Mitchell", + "age": 91, + "emails": []string{"one", "two", "three"}, + "extra": map[string]string{ + "twitter": "mitchellh", + }, + } + + var result Person + err := Decode(input, &result) + if err != nil { + panic(err) + } + + fmt.Printf("%#v", result) + // Output: + // mapstructure.Person{Name:"Mitchell", Age:91, Emails:[]string{"one", "two", "three"}, Extra:map[string]string{"twitter":"mitchellh"}} +} + +func ExampleDecode_errors() { + type Person struct { + Name string + Age int + Emails []string + Extra map[string]string + } + + // This input can come from anywhere, but typically comes from + // something like decoding JSON where we're not quite sure of the + // struct initially. + input := map[string]interface{}{ + "name": 123, + "age": "bad value", + "emails": []int{1, 2, 3}, + } + + var result Person + err := Decode(input, &result) + if err == nil { + panic("should have an error") + } + + fmt.Println(err.Error()) + // Output: + // 5 error(s) decoding: + // + // * 'Name' expected type 'string', got unconvertible type 'int' + // * 'Age' expected type 'int', got unconvertible type 'string' + // * 'Emails[0]' expected type 'string', got unconvertible type 'int' + // * 'Emails[1]' expected type 'string', got unconvertible type 'int' + // * 'Emails[2]' expected type 'string', got unconvertible type 'int' +} + +func ExampleDecode_metadata() { + type Person struct { + Name string + Age int + } + + // This input can come from anywhere, but typically comes from + // something like decoding JSON where we're not quite sure of the + // struct initially. + input := map[string]interface{}{ + "name": "Mitchell", + "age": 91, + "email": "foo@bar.com", + } + + // For metadata, we make a more advanced DecoderConfig so we can + // more finely configure the decoder that is used. In this case, we + // just tell the decoder we want to track metadata. + var md Metadata + var result Person + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + panic(err) + } + + if err := decoder.Decode(input); err != nil { + panic(err) + } + + fmt.Printf("Unused keys: %#v", md.Unused) + // Output: + // Unused keys: []string{"email"} +} + +func ExampleDecode_weaklyTypedInput() { + type Person struct { + Name string + Age int + Emails []string + } + + // This input can come from anywhere, but typically comes from + // something like decoding JSON, generated by a weakly typed language + // such as PHP. + input := map[string]interface{}{ + "name": 123, // number => string + "age": "42", // string => number + "emails": map[string]interface{}{}, // empty map => empty array + } + + var result Person + config := &DecoderConfig{ + WeaklyTypedInput: true, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + panic(err) + } + + err = decoder.Decode(input) + if err != nil { + panic(err) + } + + fmt.Printf("%#v", result) + // Output: mapstructure.Person{Name:"123", Age:42, Emails:[]string{}} +} + +func ExampleDecode_tags() { + // Note that the mapstructure tags defined in the struct type + // can indicate which fields the values are mapped to. + type Person struct { + Name string `mapstructure:"person_name"` + Age int `mapstructure:"person_age"` + } + + input := map[string]interface{}{ + "person_name": "Mitchell", + "person_age": 91, + } + + var result Person + err := Decode(input, &result) + if err != nil { + panic(err) + } + + fmt.Printf("%#v", result) + // Output: + // mapstructure.Person{Name:"Mitchell", Age:91} +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_test.go new file mode 100644 index 000000000000..23029c7c4aa0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_test.go @@ -0,0 +1,828 @@ +package mapstructure + +import ( + "reflect" + "sort" + "testing" +) + +type Basic struct { + Vstring string + Vint int + Vuint uint + Vbool bool + Vfloat float64 + Vextra string + vsilent bool + Vdata interface{} +} + +type Embedded struct { + Basic + Vunique string +} + +type EmbeddedPointer struct { + *Basic + Vunique string +} + +type EmbeddedSquash struct { + Basic `mapstructure:",squash"` + Vunique string +} + +type Map struct { + Vfoo string + Vother map[string]string +} + +type MapOfStruct struct { + Value map[string]Basic +} + +type Nested struct { + Vfoo string + Vbar Basic +} + +type NestedPointer struct { + Vfoo string + Vbar *Basic +} + +type Slice struct { + Vfoo string + Vbar []string +} + +type SliceOfStruct struct { + Value []Basic +} + +type Tagged struct { + Extra string `mapstructure:"bar,what,what"` + Value string `mapstructure:"foo"` +} + +type TypeConversionResult struct { + IntToFloat float32 + IntToUint uint + IntToBool bool + IntToString string + UintToInt int + UintToFloat float32 + UintToBool bool + UintToString string + BoolToInt int + BoolToUint uint + BoolToFloat float32 + BoolToString string + FloatToInt int + FloatToUint uint + FloatToBool bool + FloatToString string + SliceUint8ToString string + StringToInt int + StringToUint uint + StringToBool bool + StringToFloat float32 + SliceToMap map[string]interface{} + MapToSlice []interface{} +} + +func TestBasicTypes(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "Vuint": 42, + "vbool": true, + "Vfloat": 42.42, + "vsilent": true, + "vdata": 42, + } + + var result Basic + err := Decode(input, &result) + if err != nil { + t.Errorf("got an err: %s", err.Error()) + t.FailNow() + } + + if result.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vstring) + } + + if result.Vint != 42 { + t.Errorf("vint value should be 42: %#v", result.Vint) + } + + if result.Vuint != 42 { + t.Errorf("vuint value should be 42: %#v", result.Vuint) + } + + if result.Vbool != true { + t.Errorf("vbool value should be true: %#v", result.Vbool) + } + + if result.Vfloat != 42.42 { + t.Errorf("vfloat value should be 42.42: %#v", result.Vfloat) + } + + if result.Vextra != "" { + t.Errorf("vextra value should be empty: %#v", result.Vextra) + } + + if result.vsilent != false { + t.Error("vsilent should not be set, it is unexported") + } + + if result.Vdata != 42 { + t.Error("vdata should be valid") + } +} + +func TestBasic_IntWithFloat(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vint": float64(42), + } + + var result Basic + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err) + } +} + +func TestDecode_Embedded(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "Basic": map[string]interface{}{ + "vstring": "innerfoo", + }, + "vunique": "bar", + } + + var result Embedded + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vstring != "innerfoo" { + t.Errorf("vstring value should be 'innerfoo': %#v", result.Vstring) + } + + if result.Vunique != "bar" { + t.Errorf("vunique value should be 'bar': %#v", result.Vunique) + } +} + +func TestDecode_EmbeddedPointer(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "Basic": map[string]interface{}{ + "vstring": "innerfoo", + }, + "vunique": "bar", + } + + var result EmbeddedPointer + err := Decode(input, &result) + if err == nil { + t.Fatal("should get error") + } +} + +func TestDecode_EmbeddedSquash(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "vunique": "bar", + } + + var result EmbeddedSquash + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vstring) + } + + if result.Vunique != "bar" { + t.Errorf("vunique value should be 'bar': %#v", result.Vunique) + } +} + +func TestDecode_DecodeHook(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vint": "WHAT", + } + + decodeHook := func(from reflect.Kind, to reflect.Kind, v interface{}) (interface{}, error) { + if from == reflect.String && to != reflect.String { + return 5, nil + } + + return v, nil + } + + var result Basic + config := &DecoderConfig{ + DecodeHook: decodeHook, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("got an err: %s", err) + } + + if result.Vint != 5 { + t.Errorf("vint should be 5: %#v", result.Vint) + } +} + +func TestDecode_Nil(t *testing.T) { + t.Parallel() + + var input interface{} = nil + result := Basic{ + Vstring: "foo", + } + + err := Decode(input, &result) + if err != nil { + t.Fatalf("err: %s", err) + } + + if result.Vstring != "foo" { + t.Fatalf("bad: %#v", result.Vstring) + } +} + +func TestDecode_NonStruct(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "foo": "bar", + "bar": "baz", + } + + var result map[string]string + err := Decode(input, &result) + if err != nil { + t.Fatalf("err: %s", err) + } + + if result["foo"] != "bar" { + t.Fatal("foo is not bar") + } +} + +func TestDecode_TypeConversion(t *testing.T) { + input := map[string]interface{}{ + "IntToFloat": 42, + "IntToUint": 42, + "IntToBool": 1, + "IntToString": 42, + "UintToInt": 42, + "UintToFloat": 42, + "UintToBool": 42, + "UintToString": 42, + "BoolToInt": true, + "BoolToUint": true, + "BoolToFloat": true, + "BoolToString": true, + "FloatToInt": 42.42, + "FloatToUint": 42.42, + "FloatToBool": 42.42, + "FloatToString": 42.42, + "SliceUint8ToString": []uint8("foo"), + "StringToInt": "42", + "StringToUint": "42", + "StringToBool": "1", + "StringToFloat": "42.42", + "SliceToMap": []interface{}{}, + "MapToSlice": map[string]interface{}{}, + } + + expectedResultStrict := TypeConversionResult{ + IntToFloat: 42.0, + IntToUint: 42, + UintToInt: 42, + UintToFloat: 42, + BoolToInt: 0, + BoolToUint: 0, + BoolToFloat: 0, + FloatToInt: 42, + FloatToUint: 42, + } + + expectedResultWeak := TypeConversionResult{ + IntToFloat: 42.0, + IntToUint: 42, + IntToBool: true, + IntToString: "42", + UintToInt: 42, + UintToFloat: 42, + UintToBool: true, + UintToString: "42", + BoolToInt: 1, + BoolToUint: 1, + BoolToFloat: 1, + BoolToString: "1", + FloatToInt: 42, + FloatToUint: 42, + FloatToBool: true, + FloatToString: "42.42", + SliceUint8ToString: "foo", + StringToInt: 42, + StringToUint: 42, + StringToBool: true, + StringToFloat: 42.42, + SliceToMap: map[string]interface{}{}, + MapToSlice: []interface{}{}, + } + + // Test strict type conversion + var resultStrict TypeConversionResult + err := Decode(input, &resultStrict) + if err == nil { + t.Errorf("should return an error") + } + if !reflect.DeepEqual(resultStrict, expectedResultStrict) { + t.Errorf("expected %v, got: %v", expectedResultStrict, resultStrict) + } + + // Test weak type conversion + var decoder *Decoder + var resultWeak TypeConversionResult + + config := &DecoderConfig{ + WeaklyTypedInput: true, + Result: &resultWeak, + } + + decoder, err = NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("got an err: %s", err) + } + + if !reflect.DeepEqual(resultWeak, expectedResultWeak) { + t.Errorf("expected \n%#v, got: \n%#v", expectedResultWeak, resultWeak) + } +} + +func TestDecoder_ErrorUnused(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "hello", + "foo": "bar", + } + + var result Basic + config := &DecoderConfig{ + ErrorUnused: true, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err == nil { + t.Fatal("expected error") + } +} + +func TestMap(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vother": map[interface{}]interface{}{ + "foo": "foo", + "bar": "bar", + }, + } + + var result Map + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an error: %s", err) + } + + if result.Vfoo != "foo" { + t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) + } + + if result.Vother == nil { + t.Fatal("vother should not be nil") + } + + if len(result.Vother) != 2 { + t.Error("vother should have two items") + } + + if result.Vother["foo"] != "foo" { + t.Errorf("'foo' key should be foo, got: %#v", result.Vother["foo"]) + } + + if result.Vother["bar"] != "bar" { + t.Errorf("'bar' key should be bar, got: %#v", result.Vother["bar"]) + } +} + +func TestMapOfStruct(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "value": map[string]interface{}{ + "foo": map[string]string{"vstring": "one"}, + "bar": map[string]string{"vstring": "two"}, + }, + } + + var result MapOfStruct + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err) + } + + if result.Value == nil { + t.Fatal("value should not be nil") + } + + if len(result.Value) != 2 { + t.Error("value should have two items") + } + + if result.Value["foo"].Vstring != "one" { + t.Errorf("foo value should be 'one', got: %s", result.Value["foo"].Vstring) + } + + if result.Value["bar"].Vstring != "two" { + t.Errorf("bar value should be 'two', got: %s", result.Value["bar"].Vstring) + } +} + +func TestNestedType(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "vbool": true, + }, + } + + var result Nested + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vfoo != "foo" { + t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) + } + + if result.Vbar.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) + } + + if result.Vbar.Vint != 42 { + t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) + } + + if result.Vbar.Vbool != true { + t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) + } + + if result.Vbar.Vextra != "" { + t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) + } +} + +func TestNestedTypePointer(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": &map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "vbool": true, + }, + } + + var result NestedPointer + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vfoo != "foo" { + t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) + } + + if result.Vbar.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) + } + + if result.Vbar.Vint != 42 { + t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) + } + + if result.Vbar.Vbool != true { + t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) + } + + if result.Vbar.Vextra != "" { + t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) + } +} + +func TestSlice(t *testing.T) { + t.Parallel() + + inputStringSlice := map[string]interface{}{ + "vfoo": "foo", + "vbar": []string{"foo", "bar", "baz"}, + } + + inputStringSlicePointer := map[string]interface{}{ + "vfoo": "foo", + "vbar": &[]string{"foo", "bar", "baz"}, + } + + outputStringSlice := &Slice{ + "foo", + []string{"foo", "bar", "baz"}, + } + + testSliceInput(t, inputStringSlice, outputStringSlice) + testSliceInput(t, inputStringSlicePointer, outputStringSlice) +} + +func TestInvalidSlice(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": 42, + } + + result := Slice{} + err := Decode(input, &result) + if err == nil { + t.Errorf("expected failure") + } +} + +func TestSliceOfStruct(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "value": []map[string]interface{}{ + {"vstring": "one"}, + {"vstring": "two"}, + }, + } + + var result SliceOfStruct + err := Decode(input, &result) + if err != nil { + t.Fatalf("got unexpected error: %s", err) + } + + if len(result.Value) != 2 { + t.Fatalf("expected two values, got %d", len(result.Value)) + } + + if result.Value[0].Vstring != "one" { + t.Errorf("first value should be 'one', got: %s", result.Value[0].Vstring) + } + + if result.Value[1].Vstring != "two" { + t.Errorf("second value should be 'two', got: %s", result.Value[1].Vstring) + } +} + +func TestInvalidType(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": 42, + } + + var result Basic + err := Decode(input, &result) + if err == nil { + t.Fatal("error should exist") + } + + derr, ok := err.(*Error) + if !ok { + t.Fatalf("error should be kind of Error, instead: %#v", err) + } + + if derr.Errors[0] != "'Vstring' expected type 'string', got unconvertible type 'int'" { + t.Errorf("got unexpected error: %s", err) + } +} + +func TestMetadata(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": map[string]interface{}{ + "vstring": "foo", + "Vuint": 42, + "foo": "bar", + }, + "bar": "nil", + } + + var md Metadata + var result Nested + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("err: %s", err.Error()) + } + + expectedKeys := []string{"Vfoo", "Vbar.Vstring", "Vbar.Vuint", "Vbar"} + if !reflect.DeepEqual(md.Keys, expectedKeys) { + t.Fatalf("bad keys: %#v", md.Keys) + } + + expectedUnused := []string{"Vbar.foo", "bar"} + if !reflect.DeepEqual(md.Unused, expectedUnused) { + t.Fatalf("bad unused: %#v", md.Unused) + } +} + +func TestMetadata_Embedded(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "vunique": "bar", + } + + var md Metadata + var result EmbeddedSquash + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("err: %s", err.Error()) + } + + expectedKeys := []string{"Vstring", "Vunique"} + + sort.Strings(md.Keys) + if !reflect.DeepEqual(md.Keys, expectedKeys) { + t.Fatalf("bad keys: %#v", md.Keys) + } + + expectedUnused := []string{} + if !reflect.DeepEqual(md.Unused, expectedUnused) { + t.Fatalf("bad unused: %#v", md.Unused) + } +} + +func TestNonPtrValue(t *testing.T) { + t.Parallel() + + err := Decode(map[string]interface{}{}, Basic{}) + if err == nil { + t.Fatal("error should exist") + } + + if err.Error() != "result must be a pointer" { + t.Errorf("got unexpected error: %s", err) + } +} + +func TestTagged(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "foo": "bar", + "bar": "value", + } + + var result Tagged + err := Decode(input, &result) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if result.Value != "bar" { + t.Errorf("value should be 'bar', got: %#v", result.Value) + } + + if result.Extra != "value" { + t.Errorf("extra should be 'value', got: %#v", result.Extra) + } +} + +func TestWeakDecode(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "foo": "4", + "bar": "value", + } + + var result struct { + Foo int + Bar string + } + + if err := WeakDecode(input, &result); err != nil { + t.Fatalf("err: %s", err) + } + if result.Foo != 4 { + t.Fatalf("bad: %#v", result) + } + if result.Bar != "value" { + t.Fatalf("bad: %#v", result) + } +} + +func testSliceInput(t *testing.T, input map[string]interface{}, expected *Slice) { + var result Slice + err := Decode(input, &result) + if err != nil { + t.Fatalf("got error: %s", err) + } + + if result.Vfoo != expected.Vfoo { + t.Errorf("Vfoo expected '%s', got '%s'", expected.Vfoo, result.Vfoo) + } + + if result.Vbar == nil { + t.Fatalf("Vbar a slice, got '%#v'", result.Vbar) + } + + if len(result.Vbar) != len(expected.Vbar) { + t.Errorf("Vbar length should be %d, got %d", len(expected.Vbar), len(result.Vbar)) + } + + for i, v := range result.Vbar { + if v != expected.Vbar[i] { + t.Errorf( + "Vbar[%d] should be '%#v', got '%#v'", + i, expected.Vbar[i], v) + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml new file mode 100644 index 000000000000..0882a5695016 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml @@ -0,0 +1,17 @@ +language: go +install: + - go get -v -tags 'fixtures acceptance' ./... +go: + - 1.1 + - 1.2 + - 1.3 + - 1.4 + - tip +script: script/cibuild +after_success: + - go get golang.org/x/tools/cmd/cover + - go get github.com/axw/gocov/gocov + - 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 new file mode 100644 index 000000000000..9748c1ad2b11 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTING.md @@ -0,0 +1,275 @@ +# Contributing to gophercloud + +- [Getting started](#getting-started) +- [Tests](#tests) +- [Style guide](#basic-style-guide) +- [5 ways to get involved](#5-ways-to-get-involved) + +## Setting up your git workspace + +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](/README.md#how-to-install). + +2. Move into the directory that houses your local repository: + + ```bash + cd ${GOPATH}/src/github.com/rackspace/gophercloud + ``` + +3. Fork the `rackspace/gophercloud` repository and update your remote refs. You +will need to rename the `origin` remote branch to `upstream`, and add your +fork as `origin` instead: + + ```bash + git remote rename origin upstream + git remote add origin git@github.com//gophercloud + ``` + +4. Checkout the latest development branch ([click here](/branches) to see all +the branches): + + ```bash + git checkout release/v1.0.1 + ``` + +5. If you're working on something (discussed more in detail below), you will +need to checkout a new feature branch: + + ```bash + git checkout -b my-new-feature + ``` + +Another thing to bear in mind is that you will need to add a few extra +environment variables for acceptance tests - this is documented in our +[acceptance tests readme](/acceptance). + +## Tests + +When working on a new or existing feature, testing will be the backbone of your +work since it helps uncover and prevent regressions in the codebase. There are +two types of test we use in gophercloud: unit tests and acceptance tests, which +are both described below. + +### Unit tests + +Unit tests are the fine-grained tests that establish and ensure the behaviour +of individual units of functionality. We usually test on an +operation-by-operation basis (an operation typically being an API action) with +the use of mocking to set up explicit expectations. Each operation will set up +its HTTP response expectation, and then test how the system responds when fed +this controlled, pre-determined input. + +To make life easier, we've introduced a bunch of test helpers to simplify the +process of testing expectations with assertions: + +```go +import ( + "testing" + + "github.com/rackspace/gophercloud/testhelper" +) + +func TestSomething(t *testing.T) { + result, err := Operation() + + testhelper.AssertEquals(t, "foo", result.Bar) + testhelper.AssertNoErr(t, err) +} + +func TestSomethingElse(t *testing.T) { + testhelper.CheckEquals(t, "expected", "actual") +} +``` + +`AssertEquals` and `AssertNoErr` will throw a fatal error if a value does not +match an expected value or if an error has been declared, respectively. You can +also use `CheckEquals` and `CheckNoErr` for the same purpose; the only difference +being that `t.Errorf` is raised rather than `t.Fatalf`. + +Here is a truncated example of mocked HTTP responses: + +```go +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestGet(t *testing.T) { + // Setup the HTTP request multiplexer and server + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + // Test we're using the correct HTTP method + th.TestMethod(t, r, "GET") + + // Test we're setting the auth token + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + // Set the appropriate headers for our mocked response + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // Set the HTTP body + fmt.Fprintf(w, ` +{ + "network": { + "status": "ACTIVE", + "name": "private-network", + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + // Call our API operation + network, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + + // Assert no errors and equality + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Status, "ACTIVE") +} +``` + +### Acceptance tests + +As we've already mentioned, unit tests have a very narrow and confined focus - +they test small units of behaviour. Acceptance tests on the other hand have a +far larger scope: they are fully functional tests that test the entire API of a +service in one fell swoop. They don't care about unit isolation or mocking +expectations, they instead do a full run-through and consequently test how the +entire system _integrates_ together. When an API satisfies expectations, it +proves by default that the requirements for a contract have been met. + +Please be aware that acceptance tests will hit a live API - and may incur +service charges from your provider. Although most tests handle their own +teardown procedures, it is always worth manually checking that resources are +deleted after the test suite finishes. + +### Running tests + +To run all tests: + +```bash +go test ./... +``` + +To run all tests with verbose output: + +```bash +go test -v ./... +``` + +To run tests that match certain [build tags](): + +```bash +go test -tags "foo bar" ./... +``` + +To run tests for a particular sub-package: + +```bash +cd ./path/to/package && go test . +``` + +## Basic style guide + +We follow the standard formatting recommendations and language idioms set out +in the [Effective Go](https://golang.org/doc/effective_go.html) guide. It's +definitely worth reading - but the relevant sections are +[formatting](https://golang.org/doc/effective_go.html#formatting) +and [names](https://golang.org/doc/effective_go.html#names). + +## 5 ways to get involved + +There are five main ways you can get involved in our open-source project, and +each is described briefly below. Once you've made up your mind and decided on +your fix, you will need to follow the same basic steps that all submissions are +required to adhere to: + +1. [fork](https://help.github.com/articles/fork-a-repo/) the `rackspace/gophercloud` repository +2. checkout a [new branch](https://github.com/Kunena/Kunena-Forum/wiki/Create-a-new-branch-with-git-and-manage-branches) +3. submit your branch as a [pull request](https://help.github.com/articles/creating-a-pull-request/) + +### 1. Providing feedback + +On of the easiest ways to get readily involved in our project is to let us know +about your experiences using our SDK. Feedback like this is incredibly useful +to us, because it allows us to refine and change features based on what our +users want and expect of us. There are a bunch of ways to get in contact! You +can [ping us](https://developer.rackspace.com/support/) via e-mail, talk to us on irc +(#rackspace-dev on freenode), [tweet us](https://twitter.com/rackspace), or +submit an issue on our [bug tracker](/issues). Things you might like to tell us +are: + +* how easy was it to start using our SDK? +* did it meet your expectations? If not, why not? +* did our documentation help or hinder you? +* what could we improve in general? + +### 2. Fixing bugs + +If you want to start fixing open bugs, we'd really appreciate that! Bug fixing +is central to any project. The best way to get started is by heading to our +[bug tracker](https://github.com/rackspace/gophercloud/issues) and finding open +bugs that you think nobody is working on. It might be useful to comment on the +thread to see the current state of the issue and if anybody has made any +breakthroughs on it so far. + +### 3. Improving documentation + +We have three forms of documentation: + +* short README documents that briefly introduce a topic +* reference documentation on [godoc.org](http://godoc.org) that is automatically +generated from source code comments +* user documentation on our [homepage](http://gophercloud.io) that includes +getting started guides, installation guides and code samples + +If you feel that a certain section could be improved - whether it's to clarify +ambiguity, correct a technical mistake, or to fix a grammatical error - please +feel entitled to do so! We welcome doc pull requests with the same childlike +enthusiasm as any other contribution! + +### 4. Optimizing existing features + +If you would like to improve or optimize an existing feature, please be aware +that we adhere to [semantic versioning](http://semver.org) - which means that +we cannot introduce breaking changes to the API without a major version change +(v1.x -> v2.x). Making that leap is a big step, so we encourage contributors to +refactor rather than rewrite. Running tests will prevent regression and avoid +the possibility of breaking somebody's current implementation. + +Another tip is to keep the focus of your work as small as possible - try not to +introduce a change that affects lots and lots of files because it introduces +added risk and increases the cognitive load on the reviewers checking your +work. Change-sets which are easily understood and will not negatively impact +users are more likely to be integrated quickly. + +Lastly, if you're seeking to optimize a particular operation, you should try to +demonstrate a negative performance impact - perhaps using go's inbuilt +[benchmark capabilities](http://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go). + +### 5. Working on a new feature + +If you've found something we've left out, definitely feel free to start work on +introducing that feature. It's always useful to open an issue or submit a pull +request early on to indicate your intent to a core contributor - this enables +quick/early feedback and can help steer you in the right direction by avoiding +known issues. It might also help you avoid losing time implementing something +that might not ever work. One tip is to prefix your Pull Request issue title +with [wip] - then people know it's a work in progress. + +You must ensure that all of your work is well tested - both in terms of unit +and acceptance tests. Untested code will not be merged because it introduces +too much of a risk to end-users. + +Happy hacking! diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md new file mode 100644 index 000000000000..63beb30b20df --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md @@ -0,0 +1,13 @@ +Contributors +============ + +| Name | Email | +| ---- | ----- | +| Samuel A. Falvo II | +| Glen Campbell | +| Jesse Noller | +| Jon Perritt | +| Ash Wilson | +| Jamie Hannaford | +| Don Schenck | don.schenck@rackspace.com> +| Joe Topjian | diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/LICENSE b/Godeps/_workspace/src/github.com/rackspace/gophercloud/LICENSE new file mode 100644 index 000000000000..fbbbc9e4cbad --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/LICENSE @@ -0,0 +1,191 @@ +Copyright 2012-2013 Rackspace, Inc. + +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. + +------ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.md new file mode 100644 index 000000000000..19e90e0f4d5d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.md @@ -0,0 +1,160 @@ +# Gophercloud: the OpenStack SDK for Go +[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](https://travis-ci.org/rackspace/gophercloud) + +Gophercloud is a flexible SDK that allows you to consume and work with OpenStack +clouds in a simple and idiomatic way using golang. Many services are supported, +including Compute, Block Storage, Object Storage, Networking, and Identity. +Each service API is backed with getting started guides, code samples, reference +documentation, unit tests and acceptance tests. + +## Useful links + +* [Gophercloud homepage](http://gophercloud.io) +* [Reference documentation](http://godoc.org/github.com/rackspace/gophercloud) +* [Getting started guides](http://gophercloud.io/docs) +* [Effective Go](https://golang.org/doc/effective_go.html) + +## How to install + +Before installing, you need to ensure that your [GOPATH environment variable](https://golang.org/doc/code.html#GOPATH) +is pointing to an appropriate directory where you want to install Gophercloud: + +```bash +mkdir $HOME/go +export GOPATH=$HOME/go +``` + +To protect yourself against changes in your dependencies, we highly recommend choosing a +[dependency management solution](https://github.com/golang/go/wiki/PackageManagementTools) for +your projects, such as [godep](https://github.com/tools/godep). Once this is set up, you can install +Gophercloud as a dependency like so: + +```bash +go get github.com/rackspace/gophercloud + +# Edit your code to import relevant packages from "github.com/rackspace/gophercloud" + +godep save ./... +``` + +This will install all the source files you need into a `Godeps/_workspace` directory, which is +referenceable from your own source files when you use the `godep go` command. + +## Getting started + +### Credentials + +Because you'll be hitting an API, you will need to retrieve your OpenStack +credentials and either store them as environment variables or in your local Go +files. The first method is recommended because it decouples credential +information from source code, allowing you to push the latter to your version +control system without any security risk. + +You will need to retrieve the following: + +* username +* password +* tenant name or tenant ID +* a valid Keystone identity URL + +For users that have the OpenStack dashboard installed, there's a shortcut. If +you visit the `project/access_and_security` path in Horizon and click on the +"Download OpenStack RC File" button at the top right hand corner, you will +download a bash file that exports all of your access details to environment +variables. To execute the file, run `source admin-openrc.sh` and you will be +prompted for your password. + +### Authentication + +Once you have access to your credentials, you can begin plugging them into +Gophercloud. The next step is authentication, and this is handled by a base +"Provider" struct. To get one, you can either pass in your credentials +explicitly, or tell Gophercloud to use environment variables: + +```go +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/utils" +) + +// Option 1: Pass in the values yourself +opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://my-openstack.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", +} + +// Option 2: Use a utility function to retrieve all your environment variables +opts, err := openstack.AuthOptionsFromEnv() +``` + +Once you have the `opts` variable, you can pass it in and get back a +`ProviderClient` struct: + +```go +provider, err := openstack.AuthenticatedClient(opts) +``` + +The `ProviderClient` is the top-level client that all of your OpenStack services +derive from. The provider contains all of the authentication details that allow +your Go code to access the API - such as the base URL and token ID. + +### Provision a server + +Once we have a base Provider, we inject it as a dependency into each OpenStack +service. In order to work with the Compute API, we need a Compute service +client; which can be created like so: + +```go +client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), +}) +``` + +We then use this `client` for any Compute API operation we want. In our case, +we want to provision a new server - so we invoke the `Create` method and pass +in the flavor ID (hardware specification) and image ID (operating system) we're +interested in: + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +server, err := servers.Create(client, servers.CreateOpts{ + Name: "My new server!", + FlavorRef: "flavor_id", + ImageRef: "image_id", +}).Extract() +``` + +If you are unsure about what images and flavors are, you can read our [Compute +Getting Started guide](http://gophercloud.io/docs/compute). The above code +sample creates a new server with the parameters, and embodies the new resource +in the `server` variable (a +[`servers.Server`](http://godoc.org/github.com/rackspace/gophercloud) struct). + +### Next steps + +Cool! You've handled authentication, got your `ProviderClient` and provisioned +a new server. You're now ready to use more OpenStack services. + +* [Getting started with Compute](http://gophercloud.io/docs/compute) +* [Getting started with Object Storage](http://gophercloud.io/docs/object-storage) +* [Getting started with Networking](http://gophercloud.io/docs/networking) +* [Getting started with Block Storage](http://gophercloud.io/docs/block-storage) +* [Getting started with Identity](http://gophercloud.io/docs/identity) + +## Contributing + +Engaging the community and lowering barriers for contributors is something we +care a lot about. For this reason, we've taken the time to write a [contributing +guide](./CONTRIBUTING.md) for folks interested in getting involved in our project. +If you're not sure how you can get involved, feel free to submit an issue or +[contact us](https://developer.rackspace.com/support/). You don't need to be a +Go expert - all members of the community are welcome! + +## Help and feedback + +If you're struggling with something or have spotted a potential bug, feel free +to submit an issue to our [bug tracker](/issues) or [contact us directly](https://developer.rackspace.com/support/). diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md new file mode 100644 index 000000000000..76a94d570329 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/UPGRADING.md @@ -0,0 +1,338 @@ +# Upgrading to v1.0.0 + +With the arrival of this new major version increment, the unfortunate news is +that breaking changes have been introduced to existing services. The API +has been completely rewritten from the ground up to make the library more +extensible, maintainable and easy-to-use. + +Below we've compiled upgrade instructions for the various services that +existed before. If you have a specific issue that is not addressed below, +please [submit an issue](/issues/new) or +[e-mail our support team](https://developer.rackspace.com/support/). + +* [Authentication](#authentication) +* [Servers](#servers) + * [List servers](#list-servers) + * [Get server details](#get-server-details) + * [Create server](#create-server) + * [Resize server](#resize-server) + * [Reboot server](#reboot-server) + * [Update server](#update-server) + * [Rebuild server](#rebuild-server) + * [Change admin password](#change-admin-password) + * [Delete server](#delete-server) + * [Rescue server](#rescue-server) +* [Images and flavors](#images-and-flavors) + * [List images](#list-images) + * [List flavors](#list-flavors) + * [Create/delete image](#createdelete-image) +* [Other](#other) + * [List keypairs](#list-keypairs) + * [Create/delete keypair](#createdelete-keypair) + * [List IP addresses](#list-ip-addresses) + +# Authentication + +One of the major differences that this release introduces is the level of +sub-packaging to differentiate between services and providers. You now have +the option of authenticating with OpenStack and other providers (like Rackspace). + +To authenticate with a vanilla OpenStack installation, you can either specify +your credentials like this: + +```go +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" +) + +opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://my-openstack.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", +} +``` + +Or have them pulled in through environment variables, like this: + +```go +opts, err := openstack.AuthOptionsFromEnv() +``` + +Once you have your `AuthOptions` struct, you pass it in to get back a `Provider`, +like so: + +```go +provider, err := openstack.AuthenticatedClient(opts) +``` + +This provider is the top-level structure that all services are created from. + +# Servers + +Before you can interact with the Compute API, you need to retrieve a +`gophercloud.ServiceClient`. To do this: + +```go +// Define your region, etc. +opts := gophercloud.EndpointOpts{Region: "RegionOne"} + +client, err := openstack.NewComputeV2(provider, opts) +``` + +## List servers + +All operations that involve API collections (servers, flavors, images) now use +the `pagination.Pager` interface. This interface represents paginated entities +that can be iterated over. + +Once you have a Pager, you can then pass a callback function into its `EachPage` +method, and this will allow you to traverse over the collection and execute +arbitrary functionality. So, an example with list servers: + +```go +import ( + "fmt" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// We have the option of filtering the server list. If we want the full +// collection, leave it as an empty struct or nil +opts := servers.ListOpts{Name: "server_1"} + +// Retrieve a pager (i.e. a paginated collection) +pager := servers.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + serverList, err := servers.ExtractServers(page) + + // `s' will be a servers.Server struct + for _, s := range serverList { + fmt.Printf("We have a server. ID=%s, Name=%s", s.ID, s.Name) + } +}) +``` + +## Get server details + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// Get the HTTP result +response := servers.Get(client, "server_id") + +// Extract a Server struct from the response +server, err := response.Extract() +``` + +## Create server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// Define our options +opts := servers.CreateOpts{ + Name: "new_server", + FlavorRef: "flavorID", + ImageRef: "imageID", +} + +// Get our response +response := servers.Create(client, opts) + +// Extract +server, err := response.Extract() +``` + +## Change admin password + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +result := servers.ChangeAdminPassword(client, "server_id", "newPassword_&123") +``` + +## Resize server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +result := servers.Resize(client, "server_id", "new_flavor_id") +``` + +## Reboot server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// You have a choice of two reboot methods: servers.SoftReboot or servers.HardReboot +result := servers.Reboot(client, "server_id", servers.SoftReboot) +``` + +## Update server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +opts := servers.UpdateOpts{Name: "new_name"} + +server, err := servers.Update(client, "server_id", opts).Extract() +``` + +## Rebuild server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +// You have the option of specifying additional options +opts := RebuildOpts{ + Name: "new_name", + AdminPass: "admin_password", + ImageID: "image_id", + Metadata: map[string]string{"owner": "me"}, +} + +result := servers.Rebuild(client, "server_id", opts) + +// You can extract a servers.Server struct from the HTTP response +server, err := result.Extract() +``` + +## Delete server + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + +response := servers.Delete(client, "server_id") +``` + +## Rescue server + +The server rescue extension for Compute is not currently supported. + +# Images and flavors + +## List images + +As with listing servers (see above), you first retrieve a Pager, and then pass +in a callback over each page: + +```go +import ( + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/openstack/compute/v2/images" +) + +// We have the option of filtering the image list. If we want the full +// collection, leave it as an empty struct +opts := images.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", Name: "Ubuntu 12.04"} + +// Retrieve a pager (i.e. a paginated collection) +pager := images.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + imageList, err := images.ExtractImages(page) + + for _, i := range imageList { + // "i" will be an images.Image + } +}) +``` + +## List flavors + +```go +import ( + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" +) + +// We have the option of filtering the flavor list. If we want the full +// collection, leave it as an empty struct +opts := flavors.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", MinRAM: 4} + +// Retrieve a pager (i.e. a paginated collection) +pager := flavors.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + flavorList, err := networks.ExtractFlavors(page) + + for _, f := range flavorList { + // "f" will be a flavors.Flavor + } +}) +``` + +## Create/delete image + +Image management has been shifted to Glance, but unfortunately this service is +not supported as of yet. You can, however, list Compute images like so: + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/images" + +// Retrieve a pager (i.e. a paginated collection) +pager := images.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + imageList, err := images.ExtractImages(page) + + for _, i := range imageList { + // "i" will be an images.Image + } +}) +``` + +# Other + +## List keypairs + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + +// Retrieve a pager (i.e. a paginated collection) +pager := keypairs.List(client, opts) + +// Define an anonymous function to be executed on each page's iteration +err := pager.EachPage(func(page pagination.Page) (bool, error) { + keyList, err := keypairs.ExtractKeyPairs(page) + + for _, k := range keyList { + // "k" will be a keypairs.KeyPair + } +}) +``` + +## Create/delete keypairs + +To create a new keypair, you need to specify its name and, optionally, a +pregenerated OpenSSH-formatted public key. + +```go +import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + +opts := keypairs.CreateOpts{ + Name: "new_key", + PublicKey: "...", +} + +response := keypairs.Create(client, opts) + +key, err := response.Extract() +``` + +To delete an existing keypair: + +```go +response := keypairs.Delete(client, "keypair_id") +``` + +## List IP addresses + +This operation is not currently supported. diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/README.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/README.md new file mode 100644 index 000000000000..3199837c20a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/README.md @@ -0,0 +1,57 @@ +# Gophercloud Acceptance tests + +The purpose of these acceptance tests is to validate that SDK features meet +the requirements of a contract - to consumers, other parts of the library, and +to a remote API. + +> **Note:** Because every test will be run against a real API endpoint, you +> may incur bandwidth and service charges for all the resource usage. These +> tests *should* remove their remote products automatically. However, there may +> be certain cases where this does not happen; always double-check to make sure +> you have no stragglers left behind. + +### Step 1. Set environment variables + +A lot of tests rely on environment variables for configuration - so you will need +to set them before running the suite. If you're testing against pure OpenStack APIs, +you can download a file that contains all of these variables for you: just visit +the `project/access_and_security` page in your control panel and click the "Download +OpenStack RC File" button at the top right. For all other providers, you will need +to set them manually. + +#### Authentication + +|Name|Description| +|---|---| +|`OS_USERNAME`|Your API username| +|`OS_PASSWORD`|Your API password| +|`OS_AUTH_URL`|The identity URL you need to authenticate| +|`OS_TENANT_NAME`|Your API tenant name| +|`OS_TENANT_ID`|Your API tenant ID| +|`RS_USERNAME`|Your Rackspace username| +|`RS_API_KEY`|Your Rackspace API key| + +#### General + +|Name|Description| +|---|---| +|`OS_REGION_NAME`|The region you want your resources to reside in| +|`RS_REGION`|Rackspace region you want your resource to reside in| + +#### Compute + +|Name|Description| +|---|---| +|`OS_IMAGE_ID`|The ID of the image your want your server to be based on| +|`OS_FLAVOR_ID`|The ID of the flavor you want your server to be based on| +|`OS_FLAVOR_ID_RESIZE`|The ID of the flavor you want your server to be resized to| +|`RS_IMAGE_ID`|The ID of the image you want servers to be created with| +|`RS_FLAVOR_ID`|The ID of the flavor you want your server to be created with| + +### 2. Run the test suite + +From the root directory, run: + +``` +./script/acceptancetest +``` 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 new file mode 100644 index 000000000000..7741aa984168 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/snapshots_test.go @@ -0,0 +1,70 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSnapshots(t *testing.T) { + + client, err := newClient(t) + th.AssertNoErr(t, err) + + v, err := volumes.Create(client, &volumes.CreateOpts{ + Name: "gophercloud-test-volume", + Size: 1, + }).Extract() + th.AssertNoErr(t, err) + + err = volumes.WaitForStatus(client, v.ID, "available", 120) + th.AssertNoErr(t, err) + + t.Logf("Created volume: %v\n", v) + + ss, err := snapshots.Create(client, &snapshots.CreateOpts{ + Name: "gophercloud-test-snapshot", + VolumeID: v.ID, + }).Extract() + th.AssertNoErr(t, err) + + err = snapshots.WaitForStatus(client, ss.ID, "available", 120) + th.AssertNoErr(t, err) + + t.Logf("Created snapshot: %+v\n", ss) + + err = snapshots.Delete(client, ss.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = gophercloud.WaitFor(120, func() (bool, error) { + _, err := snapshots.Get(client, ss.ID).Extract() + if err != nil { + return true, nil + } + + return false, nil + }) + th.AssertNoErr(t, err) + + t.Log("Deleted snapshot\n") + + err = volumes.Delete(client, v.ID).ExtractErr() + th.AssertNoErr(t, err) + + err = gophercloud.WaitFor(120, func() (bool, error) { + _, err := volumes.Get(client, v.ID).Extract() + if err != nil { + return true, nil + } + + return false, nil + }) + th.AssertNoErr(t, err) + + t.Log("Deleted volume\n") +} 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 new file mode 100644 index 000000000000..7760427f08c0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumes_test.go @@ -0,0 +1,63 @@ +// +build acceptance blockstorage + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newClient(t *testing.T) (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func TestVolumes(t *testing.T) { + client, err := newClient(t) + th.AssertNoErr(t, err) + + cv, err := volumes.Create(client, &volumes.CreateOpts{ + Size: 1, + Name: "gophercloud-test-volume", + }).Extract() + th.AssertNoErr(t, err) + defer func() { + err = volumes.WaitForStatus(client, cv.ID, "available", 60) + th.AssertNoErr(t, err) + err = volumes.Delete(client, cv.ID).ExtractErr() + th.AssertNoErr(t, err) + }() + + _, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{ + Name: "gophercloud-updated-volume", + }).Extract() + th.AssertNoErr(t, err) + + v, err := volumes.Get(client, cv.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Got volume: %+v\n", v) + + if v.Name != "gophercloud-updated-volume" { + t.Errorf("Unable to update volume: Expected name: gophercloud-updated-volume\nActual name: %s", v.Name) + } + + err = volumes.List(client, &volumes.ListOpts{Name: "gophercloud-updated-volume"}).EachPage(func(page pagination.Page) (bool, error) { + vols, err := volumes.ExtractVolumes(page) + th.CheckEquals(t, 1, len(vols)) + return true, err + }) + th.AssertNoErr(t, err) +} 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 new file mode 100644 index 000000000000..000bc01d5751 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/blockstorage/v1/volumetypes_test.go @@ -0,0 +1,49 @@ +// +build acceptance + +package v1 + +import ( + "testing" + "time" + + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVolumeTypes(t *testing.T) { + client, err := newClient(t) + th.AssertNoErr(t, err) + + vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{ + ExtraSpecs: map[string]interface{}{ + "capabilities": "gpu", + "priority": 3, + }, + Name: "gophercloud-test-volumeType", + }).Extract() + th.AssertNoErr(t, err) + defer func() { + time.Sleep(10000 * time.Millisecond) + err = volumetypes.Delete(client, vt.ID).ExtractErr() + if err != nil { + t.Error(err) + return + } + }() + t.Logf("Created volume type: %+v\n", vt) + + vt, err = volumetypes.Get(client, vt.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Got volume type: %+v\n", vt) + + err = volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) { + volTypes, err := volumetypes.ExtractVolumeTypes(page) + if len(volTypes) != 1 { + t.Errorf("Expected 1 volume type, got %d", len(volTypes)) + } + t.Logf("Listing volume types: %+v\n", volTypes) + return true, err + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/client_test.go new file mode 100644 index 000000000000..6e88819d80e8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/client_test.go @@ -0,0 +1,40 @@ +// +build acceptance + +package openstack + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" +) + +func TestAuthenticatedClient(t *testing.T) { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + t.Fatalf("Unable to acquire credentials: %v", err) + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + t.Fatalf("Unable to authenticate: %v", err) + } + + if client.TokenID == "" { + t.Errorf("No token ID assigned to the client") + } + + t.Logf("Client successfully acquired a token: %v", client.TokenID) + + // Find the storage service in the service catalog. + storage, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + if err != nil { + t.Errorf("Unable to locate a storage service: %v", err) + } else { + t.Logf("Located a storage service at endpoint: [%s]", storage.Endpoint) + } +} 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 new file mode 100644 index 000000000000..add0e5fc11e0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/bootfromvolume_test.go @@ -0,0 +1,55 @@ +// +build acceptance + +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" +) + +func TestBootFromVolume(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + name := tools.RandomString("Gophercloud-", 8) + t.Logf("Creating server [%s].", name) + + bd := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + UUID: choices.ImageID, + SourceType: bootfromvolume.Image, + VolumeSize: 10, + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: name, + 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 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 new file mode 100644 index 000000000000..c1bbf7961f4f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/compute_test.go @@ -0,0 +1,104 @@ +// +build acceptance common + +package v2 + +import ( + "fmt" + "os" + "strings" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +func newClient() (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return nil, err + } + + return openstack.NewComputeV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func waitForStatus(client *gophercloud.ServiceClient, server *servers.Server, status string) error { + return tools.WaitFor(func() (bool, error) { + latest, err := servers.Get(client, server.ID).Extract() + if err != nil { + return false, err + } + + if latest.Status == status { + // Success! + return true, nil + } + + return false, nil + }) +} + +// ComputeChoices contains image and flavor selections for use by the acceptance tests. +type ComputeChoices struct { + // ImageID contains the ID of a valid image. + ImageID string + + // FlavorID contains the ID of a valid flavor. + FlavorID string + + // FlavorIDResize contains the ID of a different flavor available on the same OpenStack installation, that is distinct + // from FlavorID. + FlavorIDResize string + + // NetworkName is the name of a network to launch the instance on. + NetworkName string +} + +// ComputeChoicesFromEnv populates a ComputeChoices struct from environment variables. +// If any required state is missing, an `error` will be returned that enumerates the missing properties. +func ComputeChoicesFromEnv() (*ComputeChoices, error) { + imageID := os.Getenv("OS_IMAGE_ID") + flavorID := os.Getenv("OS_FLAVOR_ID") + flavorIDResize := os.Getenv("OS_FLAVOR_ID_RESIZE") + networkName := os.Getenv("OS_NETWORK_NAME") + + missing := make([]string, 0, 3) + if imageID == "" { + missing = append(missing, "OS_IMAGE_ID") + } + if flavorID == "" { + missing = append(missing, "OS_FLAVOR_ID") + } + if flavorIDResize == "" { + missing = append(missing, "OS_FLAVOR_ID_RESIZE") + } + if networkName == "" { + networkName = "public" + } + + notDistinct := "" + if flavorID == flavorIDResize { + notDistinct = "OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE must be distinct." + } + + if len(missing) > 0 || notDistinct != "" { + text := "You're missing some important setup:\n" + if len(missing) > 0 { + text += " * These environment variables must be provided: " + strings.Join(missing, ", ") + "\n" + } + if notDistinct != "" { + text += " * " + notDistinct + "\n" + } + + return nil, fmt.Errorf(text) + } + + return &ComputeChoices{ImageID: imageID, FlavorID: flavorID, FlavorIDResize: flavorIDResize, NetworkName: networkName}, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/extension_test.go new file mode 100644 index 000000000000..1356ffa89984 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/extension_test.go @@ -0,0 +1,47 @@ +// +build acceptance compute extensionss + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListExtensions(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + err = extensions.List(client).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + exts, err := extensions.ExtractExtensions(page) + th.AssertNoErr(t, err) + + for i, ext := range exts { + t.Logf("[%02d] name=[%s]\n", i, ext.Name) + t.Logf(" alias=[%s]\n", ext.Alias) + t.Logf(" description=[%s]\n", ext.Description) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func TestGetExtension(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + ext, err := extensions.Get(client, "os-admin-actions").Extract() + th.AssertNoErr(t, err) + + t.Logf("Extension details:") + t.Logf(" name=[%s]\n", ext.Name) + t.Logf(" namespace=[%s]\n", ext.Namespace) + t.Logf(" alias=[%s]\n", ext.Alias) + t.Logf(" description=[%s]\n", ext.Description) + t.Logf(" updated=[%s]\n", ext.Updated) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/flavors_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/flavors_test.go new file mode 100644 index 000000000000..9f51b12280b6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/flavors_test.go @@ -0,0 +1,57 @@ +// +build acceptance compute flavors + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" + "github.com/rackspace/gophercloud/pagination" +) + +func TestListFlavors(t *testing.T) { + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + t.Logf("ID\tRegion\tName\tStatus\tCreated") + + pager := flavors.ListDetail(client, nil) + count, pages := 0, 0 + pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("---") + pages++ + flavors, err := flavors.ExtractFlavors(page) + if err != nil { + return false, err + } + + for _, f := range flavors { + t.Logf("%s\t%s\t%d\t%d\t%d", f.ID, f.Name, f.RAM, f.Disk, f.VCPUs) + } + + return true, nil + }) + + t.Logf("--------\n%d flavors listed on %d pages.", count, pages) +} + +func TestGetFlavor(t *testing.T) { + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + flavor, err := flavors.Get(client, choices.FlavorID).Extract() + if err != nil { + t.Fatalf("Unable to get flavor information: %v", err) + } + + t.Logf("Flavor: %#v", flavor) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/floatingip_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/floatingip_test.go new file mode 100644 index 000000000000..ab7554b5a880 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/floatingip_test.go @@ -0,0 +1,107 @@ +// +build acceptance compute servers + +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func createFIPServer(t *testing.T, client *gophercloud.ServiceClient, choices *ComputeChoices) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + 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, + ImageRef: choices.ImageID, + AdminPass: pwd, + }).Extract() + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + + th.AssertEquals(t, pwd, server.AdminPass) + + return server, err +} + +func createFloatingIP(t *testing.T, client *gophercloud.ServiceClient) (*floatingip.FloatingIP, error) { + pool := os.Getenv("OS_POOL_NAME") + fip, err := floatingip.Create(client, &floatingip.CreateOpts{ + Pool: pool, + }).Extract() + th.AssertNoErr(t, err) + t.Logf("Obtained Floating IP: %v", fip.IP) + + return fip, err +} + +func associateFloatingIP(t *testing.T, client *gophercloud.ServiceClient, serverId string, fip *floatingip.FloatingIP) { + err := floatingip.Associate(client, serverId, fip.IP).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Associated floating IP %v from instance %v", fip.IP, serverId) + defer func() { + err = floatingip.Disassociate(client, serverId, fip.IP).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Disassociated floating IP %v from instance %v", fip.IP, serverId) + }() + floatingIp, err := floatingip.Get(client, fip.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Floating IP %v is associated with Fixed IP %v", fip.IP, floatingIp.FixedIP) +} + +func TestFloatingIP(t *testing.T) { + pool := os.Getenv("OS_POOL_NAME") + if pool == "" { + t.Fatalf("OS_POOL_NAME must be set") + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := createFIPServer(t, client, choices) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer func() { + servers.Delete(client, server.ID) + t.Logf("Server deleted.") + }() + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + + fip, err := createFloatingIP(t, client) + if err != nil { + t.Fatalf("Unable to create floating IP: %v", err) + } + defer func() { + err = floatingip.Delete(client, fip.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Floating IP deleted.") + }() + + associateFloatingIP(t, client, server.ID, fip) + +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/images_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/images_test.go new file mode 100644 index 000000000000..ceab22fa76de --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/images_test.go @@ -0,0 +1,37 @@ +// +build acceptance compute images + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/images" + "github.com/rackspace/gophercloud/pagination" +) + +func TestListImages(t *testing.T) { + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute: client: %v", err) + } + + t.Logf("ID\tRegion\tName\tStatus\tCreated") + + pager := images.ListDetail(client, nil) + count, pages := 0, 0 + pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + images, err := images.ExtractImages(page) + if err != nil { + return false, err + } + + for _, i := range images { + t.Logf("%s\t%s\t%s\t%s", i.ID, i.Name, i.Status, i.Created) + } + + return true, nil + }) + + t.Logf("--------\n%d images listed on %d pages.", count, pages) +} 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 000000000000..a4fe8db2d0d8 --- /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" + + "golang.org/x/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/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/pkg.go new file mode 100644 index 000000000000..bb158c3eecc2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/pkg.go @@ -0,0 +1,3 @@ +// The v2 package contains acceptance tests for the Openstack Compute V2 service. + +package v2 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 000000000000..78b07986bd28 --- /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 000000000000..4f5073910932 --- /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/servergroup_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servergroup_test.go new file mode 100644 index 000000000000..80015e143f52 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servergroup_test.go @@ -0,0 +1,58 @@ +// +build acceptance compute servers + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups" +) + +func createServerGroup(t *testing.T, computeClient *gophercloud.ServiceClient) (*servergroups.ServerGroup, error) { + sg, err := servergroups.Create(computeClient, &servergroups.CreateOpts{ + Name: "test", + Policies: []string{"affinity"}, + }).Extract() + + if err != nil { + t.Fatalf("Unable to create server group: %v", err) + } + + t.Logf("Created server group: %v", sg.ID) + t.Logf("It has policies: %v", sg.Policies) + + return sg, nil +} + +func getServerGroup(t *testing.T, computeClient *gophercloud.ServiceClient, sgID string) error { + sg, err := servergroups.Get(computeClient, sgID).Extract() + if err != nil { + t.Fatalf("Unable to get server group: %v", err) + } + + t.Logf("Got server group: %v", sg.Name) + + return nil +} + +func TestServerGroups(t *testing.T) { + computeClient, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + sg, err := createServerGroup(t, computeClient) + if err != nil { + t.Fatalf("Unable to create server group: %v", err) + } + defer func() { + servergroups.Delete(computeClient, sg.ID) + t.Logf("ServerGroup deleted.") + }() + + err = getServerGroup(t, computeClient, sg.ID) + if err != nil { + t.Fatalf("Unable to get server group: %v", err) + } +} 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 new file mode 100644 index 000000000000..7b928e9ef509 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/servers_test.go @@ -0,0 +1,478 @@ +// +build acceptance compute servers + +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack" + "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) { + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + t.Logf("ID\tRegion\tName\tStatus\tIPv4\tIPv6") + + pager := servers.List(client, servers.ListOpts{}) + count, pages := 0, 0 + pager.EachPage(func(page pagination.Page) (bool, error) { + pages++ + t.Logf("---") + + servers, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + + for _, s := range servers { + t.Logf("%s\t%s\t%s\t%s\t%s\t\n", s.ID, s.Name, s.Status, s.AccessIPv4, s.AccessIPv6) + count++ + } + + return true, nil + }) + + t.Logf("--------\n%d servers listed on %d pages.\n", count, pages) +} + +func networkingClient() (*gophercloud.ServiceClient, error) { + opts, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + provider, err := openstack.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *ComputeChoices) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + var network networks.Network + + networkingClient, err := networkingClient() + if err != nil { + t.Fatalf("Unable to create a networking client: %v", err) + } + + pager := networks.List(networkingClient, networks.ListOpts{ + Name: choices.NetworkName, + Limit: 1, + }) + pager.EachPage(func(page pagination.Page) (bool, error) { + networks, err := networks.ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + if len(networks) == 0 { + t.Fatalf("No networks to attach to server") + return false, err + } + + network = networks[0] + + return false, nil + }) + + 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, + ImageRef: choices.ImageID, + 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 +} + +func TestCreateDestroyServer(t *testing.T) { + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(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.Fatalf("Unable to create server: %v", err) + } + defer func() { + servers.Delete(client, server.ID) + t.Logf("Server deleted.") + }() + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + + pager := servers.ListAddresses(client, server.ID) + pager.EachPage(func(page pagination.Page) (bool, error) { + networks, err := servers.ExtractAddresses(page) + if err != nil { + return false, err + } + + for n, a := range networks { + t.Logf("%s: %+v\n", n, a) + } + return true, nil + }) + + pager = servers.ListAddressesByNetwork(client, server.ID, choices.NetworkName) + pager.EachPage(func(page pagination.Page) (bool, error) { + addresses, err := servers.ExtractNetworkAddresses(page) + if err != nil { + return false, err + } + + for _, a := range addresses { + t.Logf("%+v\n", a) + } + return true, nil + }) +} + +func TestUpdateServer(t *testing.T) { + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(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) + } + + alternateName := tools.RandomString("ACPTTEST", 16) + for alternateName == server.Name { + alternateName = tools.RandomString("ACPTTEST", 16) + } + + t.Logf("Attempting to rename the server to %s.", alternateName) + + updated, err := servers.Update(client, server.ID, servers.UpdateOpts{Name: alternateName}).Extract() + if err != nil { + t.Fatalf("Unable to rename server: %v", err) + } + + if updated.ID != server.ID { + t.Errorf("Updated server ID [%s] didn't match original server ID [%s]!", updated.ID, server.ID) + } + + err = tools.WaitFor(func() (bool, error) { + latest, err := servers.Get(client, updated.ID).Extract() + if err != nil { + return false, err + } + + return latest.Name == alternateName, nil + }) +} + +func TestActionChangeAdminPassword(t *testing.T) { + t.Parallel() + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(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) + } + + randomPassword := tools.MakeNewPassword(server.AdminPass) + res := servers.ChangeAdminPassword(client, server.ID, randomPassword) + if res.Err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "PASSWORD"); err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestActionReboot(t *testing.T) { + t.Parallel() + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(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) + } + + res := servers.Reboot(client, server.ID, "aldhjflaskhjf") + if res.Err == nil { + t.Fatal("Expected the SDK to provide an ArgumentError here") + } + + t.Logf("Attempting reboot of server %s", server.ID) + res = servers.Reboot(client, server.ID, servers.OSReboot) + if res.Err != nil { + t.Fatalf("Unable to reboot server: %v", err) + } + + if err = waitForStatus(client, server, "REBOOT"); err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestActionRebuild(t *testing.T) { + t.Parallel() + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(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) + } + + t.Logf("Attempting to rebuild server %s", server.ID) + + rebuildOpts := servers.RebuildOpts{ + Name: tools.RandomString("ACPTTEST", 16), + AdminPass: tools.MakeNewPassword(server.AdminPass), + ImageID: choices.ImageID, + } + + rebuilt, err := servers.Rebuild(client, server.ID, rebuildOpts).Extract() + if err != nil { + t.Fatal(err) + } + + if rebuilt.ID != server.ID { + t.Errorf("Expected rebuilt server ID of [%s]; got [%s]", server.ID, rebuilt.ID) + } + + if err = waitForStatus(client, rebuilt, "REBUILD"); err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, rebuilt, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func resizeServer(t *testing.T, client *gophercloud.ServiceClient, server *servers.Server, choices *ComputeChoices) { + if err := waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } + + t.Logf("Attempting to resize server [%s]", server.ID) + + opts := &servers.ResizeOpts{ + FlavorRef: choices.FlavorIDResize, + } + if res := servers.Resize(client, server.ID, opts); res.Err != nil { + t.Fatal(res.Err) + } + + if err := waitForStatus(client, server, "VERIFY_RESIZE"); err != nil { + t.Fatal(err) + } +} + +func TestActionResizeConfirm(t *testing.T) { + t.Parallel() + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(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) + resizeServer(t, client, server, choices) + + t.Logf("Attempting to confirm resize for server %s", server.ID) + + if res := servers.ConfirmResize(client, server.ID); res.Err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatal(err) + } +} + +func TestActionResizeRevert(t *testing.T) { + t.Parallel() + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(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) + resizeServer(t, client, server, choices) + + t.Logf("Attempting to revert resize for server %s", server.ID) + + if res := servers.RevertResize(client, server.ID); res.Err != nil { + t.Fatal(err) + } + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + 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/compute/v2/tenantnetworks_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/tenantnetworks_test.go new file mode 100644 index 000000000000..a92e8bf53145 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/tenantnetworks_test.go @@ -0,0 +1,109 @@ +// +build acceptance compute servers + +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func getNetworkID(t *testing.T, client *gophercloud.ServiceClient, networkName string) (string, error) { + allPages, err := tenantnetworks.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + networkList, err := tenantnetworks.ExtractNetworks(allPages) + if err != nil { + t.Fatalf("Unable to list networks: %v", err) + } + + networkID := "" + for _, network := range networkList { + t.Logf("Network: %v", network) + if network.Name == networkName { + networkID = network.ID + } + } + + t.Logf("Found network ID for %s: %s\n", networkName, networkID) + + return networkID, nil +} + +func createNetworkServer(t *testing.T, client *gophercloud.ServiceClient, choices *ComputeChoices, networkID string) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s\n", name) + + pwd := tools.MakeNewPassword("") + + networks := make([]servers.Network, 1) + networks[0] = servers.Network{ + UUID: networkID, + } + + server, err := servers.Create(client, servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + AdminPass: pwd, + Networks: networks, + }).Extract() + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + + th.AssertEquals(t, pwd, server.AdminPass) + + return server, err +} + +func TestTenantNetworks(t *testing.T) { + networkName := os.Getenv("OS_NETWORK_NAME") + if networkName == "" { + t.Fatalf("OS_NETWORK_NAME must be set") + } + + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + client, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + networkID, err := getNetworkID(t, client, networkName) + if err != nil { + t.Fatalf("Unable to get network ID: %v", err) + } + + server, err := createNetworkServer(t, client, choices, networkID) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer func() { + servers.Delete(client, server.ID) + t.Logf("Server deleted.") + }() + + if err = waitForStatus(client, server, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + + allPages, err := tenantnetworks.List(client).AllPages() + allNetworks, err := tenantnetworks.ExtractNetworks(allPages) + th.AssertNoErr(t, err) + t.Logf("Retrieved all %d networks: %+v", len(allNetworks), allNetworks) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/volumeattach_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/volumeattach_test.go new file mode 100644 index 000000000000..34634c9d2f8b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/compute/v2/volumeattach_test.go @@ -0,0 +1,125 @@ +// +build acceptance compute servers + +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newBlockClient(t *testing.T) (*gophercloud.ServiceClient, error) { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +func createVAServer(t *testing.T, computeClient *gophercloud.ServiceClient, choices *ComputeChoices) (*servers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s\n", name) + + pwd := tools.MakeNewPassword("") + + server, err := servers.Create(computeClient, servers.CreateOpts{ + Name: name, + FlavorRef: choices.FlavorID, + ImageRef: choices.ImageID, + AdminPass: pwd, + }).Extract() + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + + th.AssertEquals(t, pwd, server.AdminPass) + + return server, err +} + +func createVAVolume(t *testing.T, blockClient *gophercloud.ServiceClient) (*volumes.Volume, error) { + volume, err := volumes.Create(blockClient, &volumes.CreateOpts{ + Size: 1, + Name: "gophercloud-test-volume", + }).Extract() + th.AssertNoErr(t, err) + defer func() { + err = volumes.WaitForStatus(blockClient, volume.ID, "available", 60) + th.AssertNoErr(t, err) + }() + + return volume, err +} + +func createVolumeAttachment(t *testing.T, computeClient *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, serverId string, volumeId string) { + va, err := volumeattach.Create(computeClient, serverId, &volumeattach.CreateOpts{ + VolumeID: volumeId, + }).Extract() + th.AssertNoErr(t, err) + defer func() { + err = volumes.WaitForStatus(blockClient, volumeId, "in-use", 60) + th.AssertNoErr(t, err) + err = volumeattach.Delete(computeClient, serverId, va.ID).ExtractErr() + th.AssertNoErr(t, err) + err = volumes.WaitForStatus(blockClient, volumeId, "available", 60) + th.AssertNoErr(t, err) + }() +} + +func TestAttachVolume(t *testing.T) { + choices, err := ComputeChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + computeClient, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + blockClient, err := newBlockClient(t) + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + server, err := createVAServer(t, computeClient, choices) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer func() { + servers.Delete(computeClient, server.ID) + t.Logf("Server deleted.") + }() + + if err = waitForStatus(computeClient, server, "ACTIVE"); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + + volume, err := createVAVolume(t, blockClient) + if err != nil { + t.Fatalf("Unable to create volume: %v", err) + } + defer func() { + err = volumes.Delete(blockClient, volume.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Volume deleted.") + }() + + createVolumeAttachment(t, computeClient, blockClient, server.ID, volume.ID) + +} 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 new file mode 100644 index 000000000000..d1fa1e3dcecd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/extension_test.go @@ -0,0 +1,46 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + extensions2 "github.com/rackspace/gophercloud/openstack/identity/v2/extensions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestEnumerateExtensions(t *testing.T) { + service := authenticatedClient(t) + + t.Logf("Extensions available on this identity endpoint:") + count := 0 + err := extensions2.List(service).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + extensions, err := extensions2.ExtractExtensions(page) + th.AssertNoErr(t, err) + + for i, ext := range extensions { + t.Logf("[%02d] name=[%s] namespace=[%s]", i, ext.Name, ext.Namespace) + t.Logf(" alias=[%s] updated=[%s]", ext.Alias, ext.Updated) + t.Logf(" description=[%s]", ext.Description) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) +} + +func TestGetExtension(t *testing.T) { + service := authenticatedClient(t) + + ext, err := extensions2.Get(service, "OS-KSCRUD").Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, "OpenStack Keystone User CRUD", ext.Name) + th.CheckEquals(t, "http://docs.openstack.org/identity/api/ext/OS-KSCRUD/v1.0", ext.Namespace) + th.CheckEquals(t, "OS-KSCRUD", ext.Alias) + th.CheckEquals(t, "OpenStack extensions to Keystone v2.0 API enabling User Operations.", ext.Description) +} 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 new file mode 100644 index 000000000000..96bf1fdadeac --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/identity_test.go @@ -0,0 +1,47 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +func v2AuthOptions(t *testing.T) gophercloud.AuthOptions { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + // Trim out unused fields. Prefer authentication by API key to password. + ao.UserID, ao.DomainID, ao.DomainName = "", "", "" + if ao.APIKey != "" { + ao.Password = "" + } + + return ao +} + +func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient { + ao := v2AuthOptions(t) + + provider, err := openstack.NewClient(ao.IdentityEndpoint) + th.AssertNoErr(t, err) + + if auth { + err = openstack.AuthenticateV2(provider, ao) + th.AssertNoErr(t, err) + } + + return openstack.NewIdentityV2(provider) +} + +func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient { + return createClient(t, false) +} + +func authenticatedClient(t *testing.T) *gophercloud.ServiceClient { + return createClient(t, true) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/pkg.go new file mode 100644 index 000000000000..5ec3cc8e833b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/pkg.go @@ -0,0 +1 @@ +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 000000000000..ba243fe02bf1 --- /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 new file mode 100644 index 000000000000..578fc483b8ef --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/tenant_test.go @@ -0,0 +1,32 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + tenants2 "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestEnumerateTenants(t *testing.T) { + service := authenticatedClient(t) + + t.Logf("Tenants to which your current token grants access:") + count := 0 + err := tenants2.List(service, nil).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + tenants, err := tenants2.ExtractTenants(page) + th.AssertNoErr(t, err) + for i, tenant := range tenants { + t.Logf("[%02d] name=[%s] id=[%s] description=[%s] enabled=[%v]", + i, tenant.Name, tenant.ID, tenant.Description, tenant.Enabled) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) +} 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 new file mode 100644 index 000000000000..d90314085588 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v2/token_test.go @@ -0,0 +1,38 @@ +// +build acceptance identity + +package v2 + +import ( + "testing" + + tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticate(t *testing.T) { + ao := v2AuthOptions(t) + service := unauthenticatedClient(t) + + // Authenticated! + result := tokens2.Create(service, tokens2.WrapOptions(ao)) + + // Extract and print the token. + token, err := result.ExtractToken() + th.AssertNoErr(t, err) + + t.Logf("Acquired token: [%s]", token.ID) + t.Logf("The token will expire at: [%s]", token.ExpiresAt.String()) + t.Logf("The token is valid for tenant: [%#v]", token.Tenant) + + // Extract and print the service catalog. + catalog, err := result.ExtractServiceCatalog() + th.AssertNoErr(t, err) + + t.Logf("Acquired service catalog listing [%d] services", len(catalog.Entries)) + for i, entry := range catalog.Entries { + t.Logf("[%02d]: name=[%s], type=[%s]", i, entry.Name, entry.Type) + for _, endpoint := range entry.Endpoints { + t.Logf(" - region=[%s] publicURL=[%s]", endpoint.Region, endpoint.PublicURL) + } + } +} 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 000000000000..fe73d1989874 --- /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/identity/v3/endpoint_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go new file mode 100644 index 000000000000..ea893c2deae8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/endpoint_test.go @@ -0,0 +1,111 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints" + services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services" + "github.com/rackspace/gophercloud/pagination" +) + +func TestListEndpoints(t *testing.T) { + // Create a service client. + serviceClient := createAuthenticatedClient(t) + if serviceClient == nil { + return + } + + // Use the service to list all available endpoints. + pager := endpoints3.List(serviceClient, endpoints3.ListOpts{}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + endpoints, err := endpoints3.ExtractEndpoints(page) + if err != nil { + t.Fatalf("Error extracting endpoings: %v", err) + } + + for _, endpoint := range endpoints { + t.Logf("Endpoint: %8s %10s %9s %s", + endpoint.ID, + endpoint.Availability, + endpoint.Name, + endpoint.URL) + } + + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error while iterating endpoint pages: %v", err) + } +} + +func TestNavigateCatalog(t *testing.T) { + // Create a service client. + client := createAuthenticatedClient(t) + if client == nil { + return + } + + var compute *services3.Service + var endpoint *endpoints3.Endpoint + + // Discover the service we're interested in. + servicePager := services3.List(client, services3.ListOpts{ServiceType: "compute"}) + err := servicePager.EachPage(func(page pagination.Page) (bool, error) { + part, err := services3.ExtractServices(page) + if err != nil { + return false, err + } + if compute != nil { + t.Fatalf("Expected one service, got more than one page") + return false, nil + } + if len(part) != 1 { + t.Fatalf("Expected one service, got %d", len(part)) + return false, nil + } + + compute = &part[0] + return true, nil + }) + if err != nil { + t.Fatalf("Unexpected error iterating pages: %v", err) + } + + if compute == nil { + t.Fatalf("No compute service found.") + } + + // Enumerate the endpoints available for this service. + computePager := endpoints3.List(client, endpoints3.ListOpts{ + Availability: gophercloud.AvailabilityPublic, + ServiceID: compute.ID, + }) + err = computePager.EachPage(func(page pagination.Page) (bool, error) { + part, err := endpoints3.ExtractEndpoints(page) + if err != nil { + return false, err + } + if endpoint != nil { + t.Fatalf("Expected one endpoint, got more than one page") + return false, nil + } + if len(part) != 1 { + t.Fatalf("Expected one endpoint, got %d", len(part)) + return false, nil + } + + endpoint = &part[0] + return true, nil + }) + + if endpoint == nil { + t.Fatalf("No endpoint found.") + } + + t.Logf("Success. The compute endpoint is at %s.", endpoint.URL) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/identity_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/identity_test.go new file mode 100644 index 000000000000..ce643458864f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/identity_test.go @@ -0,0 +1,39 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +func createAuthenticatedClient(t *testing.T) *gophercloud.ServiceClient { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + // Trim out unused fields. + ao.Username, ao.TenantID, ao.TenantName = "", "", "" + + if ao.UserID == "" { + t.Logf("Skipping identity v3 tests because no OS_USERID is present.") + return nil + } + + // Create a client and manually authenticate against v3. + providerClient, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + t.Fatalf("Unable to instantiate client: %v", err) + } + + err = openstack.AuthenticateV3(providerClient, ao) + if err != nil { + t.Fatalf("Unable to authenticate against identity v3: %v", err) + } + + // Create a service client. + return openstack.NewIdentityV3(providerClient) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/pkg.go new file mode 100644 index 000000000000..eac3ae96a1ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/pkg.go @@ -0,0 +1 @@ +package v3 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/service_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/service_test.go new file mode 100644 index 000000000000..082bd11e742f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/service_test.go @@ -0,0 +1,36 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services" + "github.com/rackspace/gophercloud/pagination" +) + +func TestListServices(t *testing.T) { + // Create a service client. + serviceClient := createAuthenticatedClient(t) + if serviceClient == nil { + return + } + + // Use the client to list all available services. + pager := services3.List(serviceClient, services3.ListOpts{}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + parts, err := services3.ExtractServices(page) + if err != nil { + return false, err + } + + t.Logf("--- Page ---") + for _, service := range parts { + t.Logf("Service: %32s %15s %10s %s", service.ID, service.Type, service.Name, *service.Description) + } + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error traversing pages: %v", err) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/token_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/token_test.go new file mode 100644 index 000000000000..4342ade03ccf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/identity/v3/token_test.go @@ -0,0 +1,42 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack" + tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens" +) + +func TestGetToken(t *testing.T) { + // Obtain credentials from the environment. + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + t.Fatalf("Unable to acquire credentials: %v", err) + } + + // Trim out unused fields. Skip if we don't have a UserID. + ao.Username, ao.TenantID, ao.TenantName = "", "", "" + if ao.UserID == "" { + t.Logf("Skipping identity v3 tests because no OS_USERID is present.") + return + } + + // Create an unauthenticated client. + provider, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + t.Fatalf("Unable to instantiate client: %v", err) + } + + // Create a service client. + service := openstack.NewIdentityV3(provider) + + // Use the service to create a token. + token, err := tokens3.Create(service, ao, nil).Extract() + if err != nil { + t.Fatalf("Unable to get token: %v", err) + } + + t.Logf("Acquired token: %s", token.ID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go new file mode 100644 index 000000000000..99e1d011875e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/apiversion_test.go @@ -0,0 +1,51 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/apiversions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListAPIVersions(t *testing.T) { + Setup(t) + defer Teardown() + + pager := apiversions.ListVersions(Client) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + versions, err := apiversions.ExtractAPIVersions(page) + th.AssertNoErr(t, err) + + for _, v := range versions { + t.Logf("API Version: ID [%s] Status [%s]", v.ID, v.Status) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} + +func TestListAPIResources(t *testing.T) { + Setup(t) + defer Teardown() + + pager := apiversions.ListVersionResources(Client, "v2.0") + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + vrs, err := apiversions.ExtractVersionResources(page) + th.AssertNoErr(t, err) + + for _, vr := range vrs { + t.Logf("Network: Name [%s] Collection [%s]", vr.Name, vr.Collection) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/common.go new file mode 100644 index 000000000000..1efac2c081a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/common.go @@ -0,0 +1,39 @@ +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +var Client *gophercloud.ServiceClient + +func NewClient() (*gophercloud.ServiceClient, error) { + opts, err := openstack.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + + provider, err := openstack.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Name: "neutron", + Region: os.Getenv("OS_REGION_NAME"), + }) +} + +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/openstack/networking/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extension_test.go new file mode 100644 index 000000000000..edcbba4fd15f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extension_test.go @@ -0,0 +1,45 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListExts(t *testing.T) { + Setup(t) + defer Teardown() + + pager := extensions.List(Client) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + exts, err := extensions.ExtractExtensions(page) + th.AssertNoErr(t, err) + + for _, ext := range exts { + t.Logf("Extension: Name [%s] Description [%s]", ext.Name, ext.Description) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} + +func TestGetExt(t *testing.T) { + Setup(t) + defer Teardown() + + ext, err := extensions.Get(Client, "service-type").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ext.Updated, "2013-01-20T00:00:00-00:00") + th.AssertEquals(t, ext.Name, "Neutron Service Type Management") + th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/neutron/service-type/api/v1.0") + th.AssertEquals(t, ext.Alias, "service-type") + th.AssertEquals(t, ext.Description, "API for retrieving service providers for Neutron advanced services") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go new file mode 100644 index 000000000000..80246b6481e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go @@ -0,0 +1,116 @@ +// +build acceptance networking fwaas + +package fwaas + +import ( + "testing" + "time" + + "github.com/rackspace/gophercloud" + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func firewallSetup(t *testing.T) string { + base.Setup(t) + return createPolicy(t, &policies.CreateOpts{}) +} + +func firewallTeardown(t *testing.T, policyID string) { + defer base.Teardown() + deletePolicy(t, policyID) +} + +func TestFirewall(t *testing.T) { + policyID := firewallSetup(t) + defer firewallTeardown(t, policyID) + + firewallID := createFirewall(t, &firewalls.CreateOpts{ + Name: "gophercloud test", + Description: "acceptance test", + PolicyID: policyID, + }) + + waitForFirewallToBeActive(t, firewallID) + + listFirewalls(t) + + updateFirewall(t, firewallID, &firewalls.UpdateOpts{ + Description: "acceptance test updated", + }) + + waitForFirewallToBeActive(t, firewallID) + + deleteFirewall(t, firewallID) + + waitForFirewallToBeDeleted(t, firewallID) +} + +func createFirewall(t *testing.T, opts *firewalls.CreateOpts) string { + f, err := firewalls.Create(base.Client, *opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created firewall: %#v", opts) + return f.ID +} + +func listFirewalls(t *testing.T) { + err := firewalls.List(base.Client, firewalls.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + firewallList, err := firewalls.ExtractFirewalls(page) + if err != nil { + t.Errorf("Failed to extract firewalls: %v", err) + return false, err + } + + for _, r := range firewallList { + t.Logf("Listing firewalls: ID [%s]", r.ID) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func updateFirewall(t *testing.T, firewallID string, opts *firewalls.UpdateOpts) { + f, err := firewalls.Update(base.Client, firewallID, *opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Updated firewall ID [%s]", f.ID) +} + +func getFirewall(t *testing.T, firewallID string) *firewalls.Firewall { + f, err := firewalls.Get(base.Client, firewallID).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting firewall ID [%s]", f.ID) + return f +} + +func deleteFirewall(t *testing.T, firewallID string) { + res := firewalls.Delete(base.Client, firewallID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted firewall %s", firewallID) +} + +func waitForFirewallToBeActive(t *testing.T, firewallID string) { + for i := 0; i < 10; i++ { + fw := getFirewall(t, firewallID) + if fw.Status == "ACTIVE" { + break + } + time.Sleep(time.Second) + } +} + +func waitForFirewallToBeDeleted(t *testing.T, firewallID string) { + for i := 0; i < 10; i++ { + err := firewalls.Get(base.Client, firewallID).Err + if err != nil { + httpStatus := err.(*gophercloud.UnexpectedResponseCodeError) + if httpStatus.Actual == 404 { + return + } + } + time.Sleep(time.Second) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go new file mode 100644 index 000000000000..206bf3313a8d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go @@ -0,0 +1 @@ +package fwaas diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go new file mode 100644 index 000000000000..fdca22e3fbce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go @@ -0,0 +1,107 @@ +// +build acceptance networking fwaas + +package fwaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func firewallPolicySetup(t *testing.T) string { + base.Setup(t) + return createRule(t, &rules.CreateOpts{ + Protocol: "tcp", + Action: "allow", + }) +} + +func firewallPolicyTeardown(t *testing.T, ruleID string) { + defer base.Teardown() + deleteRule(t, ruleID) +} + +func TestFirewallPolicy(t *testing.T) { + ruleID := firewallPolicySetup(t) + defer firewallPolicyTeardown(t, ruleID) + + policyID := createPolicy(t, &policies.CreateOpts{ + Name: "gophercloud test", + Description: "acceptance test", + Rules: []string{ + ruleID, + }, + }) + + listPolicies(t) + + updatePolicy(t, policyID, &policies.UpdateOpts{ + Description: "acceptance test updated", + }) + + getPolicy(t, policyID) + + removeRuleFromPolicy(t, policyID, ruleID) + + addRuleToPolicy(t, policyID, ruleID) + + deletePolicy(t, policyID) +} + +func createPolicy(t *testing.T, opts *policies.CreateOpts) string { + p, err := policies.Create(base.Client, *opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created policy: %#v", opts) + return p.ID +} + +func listPolicies(t *testing.T) { + err := policies.List(base.Client, policies.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + policyList, err := policies.ExtractPolicies(page) + if err != nil { + t.Errorf("Failed to extract policies: %v", err) + return false, err + } + + for _, p := range policyList { + t.Logf("Listing policies: ID [%s]", p.ID) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func updatePolicy(t *testing.T, policyID string, opts *policies.UpdateOpts) { + p, err := policies.Update(base.Client, policyID, *opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Updated policy ID [%s]", p.ID) +} + +func removeRuleFromPolicy(t *testing.T, policyID string, ruleID string) { + err := policies.RemoveRule(base.Client, policyID, ruleID) + th.AssertNoErr(t, err) + t.Logf("Removed rule [%s] from policy ID [%s]", ruleID, policyID) +} + +func addRuleToPolicy(t *testing.T, policyID string, ruleID string) { + err := policies.InsertRule(base.Client, policyID, ruleID, "", "") + th.AssertNoErr(t, err) + t.Logf("Inserted rule [%s] into policy ID [%s]", ruleID, policyID) +} + +func getPolicy(t *testing.T, policyID string) { + p, err := policies.Get(base.Client, policyID).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting policy ID [%s]", p.ID) +} + +func deletePolicy(t *testing.T, policyID string) { + res := policies.Delete(base.Client, policyID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted policy %s", policyID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go new file mode 100644 index 000000000000..144aa0998f1b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go @@ -0,0 +1,84 @@ +// +build acceptance networking fwaas + +package fwaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestFirewallRules(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + ruleID := createRule(t, &rules.CreateOpts{ + Name: "gophercloud_test", + Description: "acceptance test", + Protocol: "tcp", + Action: "allow", + DestinationIPAddress: "192.168.0.0/24", + DestinationPort: "22", + }) + + listRules(t) + + destinationIPAddress := "192.168.1.0/24" + destinationPort := "" + sourcePort := "1234" + + updateRule(t, ruleID, &rules.UpdateOpts{ + DestinationIPAddress: &destinationIPAddress, + DestinationPort: &destinationPort, + SourcePort: &sourcePort, + }) + + getRule(t, ruleID) + + deleteRule(t, ruleID) +} + +func createRule(t *testing.T, opts *rules.CreateOpts) string { + r, err := rules.Create(base.Client, *opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created rule: %#v", opts) + return r.ID +} + +func listRules(t *testing.T) { + err := rules.List(base.Client, rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + ruleList, err := rules.ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract rules: %v", err) + return false, err + } + + for _, r := range ruleList { + t.Logf("Listing rules: ID [%s]", r.ID) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func updateRule(t *testing.T, ruleID string, opts *rules.UpdateOpts) { + r, err := rules.Update(base.Client, ruleID, *opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Updated rule ID [%s]", r.ID) +} + +func getRule(t *testing.T, ruleID string) { + r, err := rules.Get(base.Client, ruleID).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting rule ID [%s]", r.ID) +} + +func deleteRule(t *testing.T, ruleID string) { + res := rules.Delete(base.Client, ruleID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted rule %s", ruleID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/layer3_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/layer3_test.go new file mode 100644 index 000000000000..63e0be39d7b9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/layer3_test.go @@ -0,0 +1,300 @@ +// +build acceptance networking layer3ext + +package extensions + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +const ( + cidr1 = "10.0.0.1/24" + cidr2 = "20.0.0.1/24" +) + +func TestAll(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + testRouter(t) + testFloatingIP(t) +} + +func testRouter(t *testing.T) { + // Setup: Create network + networkID := createNetwork(t) + + // Create router + routerID := createRouter(t, networkID) + + // Lists routers + listRouters(t) + + // Update router + updateRouter(t, routerID) + + // Get router + getRouter(t, routerID) + + // Create new subnet. Note: this subnet will be deleted when networkID is deleted + subnetID := createSubnet(t, networkID, cidr2) + + // Add interface + addInterface(t, routerID, subnetID) + + // Remove interface + removeInterface(t, routerID, subnetID) + + // Delete router + deleteRouter(t, routerID) + + // Cleanup + deleteNetwork(t, networkID) +} + +func testFloatingIP(t *testing.T) { + // Setup external network + extNetworkID := createNetwork(t) + + // Setup internal network, subnet and port + intNetworkID, subnetID, portID := createInternalTopology(t) + + // Now the important part: we need to allow the external network to talk to + // the internal subnet. For this we need a router that has an interface to + // the internal subnet. + routerID := bridgeIntSubnetWithExtNetwork(t, extNetworkID, subnetID) + + // Create floating IP + ipID := createFloatingIP(t, extNetworkID, portID) + + // Get floating IP + getFloatingIP(t, ipID) + + // Update floating IP + updateFloatingIP(t, ipID, portID) + + // Delete floating IP + deleteFloatingIP(t, ipID) + + // Remove the internal subnet interface + removeInterface(t, routerID, subnetID) + + // Delete router and external network + deleteRouter(t, routerID) + deleteNetwork(t, extNetworkID) + + // Delete internal port and network + deletePort(t, portID) + deleteNetwork(t, intNetworkID) +} + +func createNetwork(t *testing.T) string { + t.Logf("Creating a network") + + asu := true + opts := external.CreateOpts{ + Parent: networks.CreateOpts{Name: "sample_network", AdminStateUp: &asu}, + External: true, + } + n, err := networks.Create(base.Client, opts).Extract() + + th.AssertNoErr(t, err) + + if n.ID == "" { + t.Fatalf("No ID returned when creating a network") + } + + createSubnet(t, n.ID, cidr1) + + t.Logf("Network created: ID [%s]", n.ID) + + return n.ID +} + +func deleteNetwork(t *testing.T, networkID string) { + t.Logf("Deleting network %s", networkID) + networks.Delete(base.Client, networkID) +} + +func deletePort(t *testing.T, portID string) { + t.Logf("Deleting port %s", portID) + ports.Delete(base.Client, portID) +} + +func createInternalTopology(t *testing.T) (string, string, string) { + t.Logf("Creating an internal network (for port)") + opts := networks.CreateOpts{Name: "internal_network"} + n, err := networks.Create(base.Client, opts).Extract() + th.AssertNoErr(t, err) + + // A subnet is also needed + subnetID := createSubnet(t, n.ID, cidr2) + + t.Logf("Creating an internal port on network %s", n.ID) + p, err := ports.Create(base.Client, ports.CreateOpts{ + NetworkID: n.ID, + Name: "fixed_internal_port", + }).Extract() + th.AssertNoErr(t, err) + + return n.ID, subnetID, p.ID +} + +func bridgeIntSubnetWithExtNetwork(t *testing.T, networkID, subnetID string) string { + // Create router with external gateway info + routerID := createRouter(t, networkID) + + // Add interface for internal subnet + addInterface(t, routerID, subnetID) + + return routerID +} + +func createSubnet(t *testing.T, networkID, cidr string) string { + t.Logf("Creating a subnet for network %s", networkID) + + iFalse := false + s, err := subnets.Create(base.Client, subnets.CreateOpts{ + NetworkID: networkID, + CIDR: cidr, + IPVersion: subnets.IPv4, + Name: "my_subnet", + EnableDHCP: &iFalse, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Subnet created: ID [%s]", s.ID) + + return s.ID +} + +func createRouter(t *testing.T, networkID string) string { + t.Logf("Creating a router for network %s", networkID) + + asu := false + gwi := routers.GatewayInfo{NetworkID: networkID} + r, err := routers.Create(base.Client, routers.CreateOpts{ + Name: "foo_router", + AdminStateUp: &asu, + GatewayInfo: &gwi, + }).Extract() + + th.AssertNoErr(t, err) + + if r.ID == "" { + t.Fatalf("No ID returned when creating a router") + } + + t.Logf("Router created: ID [%s]", r.ID) + + return r.ID +} + +func listRouters(t *testing.T) { + pager := routers.List(base.Client, routers.ListOpts{}) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + routerList, err := routers.ExtractRouters(page) + th.AssertNoErr(t, err) + + for _, r := range routerList { + t.Logf("Listing router: ID [%s] Name [%s] Status [%s] GatewayInfo [%#v]", + r.ID, r.Name, r.Status, r.GatewayInfo) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updateRouter(t *testing.T, routerID string) { + _, err := routers.Update(base.Client, routerID, routers.UpdateOpts{ + Name: "another_name", + }).Extract() + + th.AssertNoErr(t, err) +} + +func getRouter(t *testing.T, routerID string) { + r, err := routers.Get(base.Client, routerID).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Getting router: ID [%s] Name [%s] Status [%s]", r.ID, r.Name, r.Status) +} + +func addInterface(t *testing.T, routerID, subnetID string) { + ir, err := routers.AddInterface(base.Client, routerID, routers.InterfaceOpts{SubnetID: subnetID}).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Interface added to router %s: SubnetID [%s] PortID [%s]", routerID, ir.SubnetID, ir.PortID) +} + +func removeInterface(t *testing.T, routerID, subnetID string) { + ir, err := routers.RemoveInterface(base.Client, routerID, routers.InterfaceOpts{SubnetID: subnetID}).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Interface %s removed from %s", ir.ID, routerID) +} + +func deleteRouter(t *testing.T, routerID string) { + t.Logf("Deleting router %s", routerID) + + res := routers.Delete(base.Client, routerID) + + th.AssertNoErr(t, res.Err) +} + +func createFloatingIP(t *testing.T, networkID, portID string) string { + t.Logf("Creating floating IP on network [%s] with port [%s]", networkID, portID) + + opts := floatingips.CreateOpts{ + FloatingNetworkID: networkID, + PortID: portID, + } + + ip, err := floatingips.Create(base.Client, opts).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Floating IP created: ID [%s] Status [%s] Fixed (internal) IP: [%s] Floating (external) IP: [%s]", + ip.ID, ip.Status, ip.FixedIP, ip.FloatingIP) + + return ip.ID +} + +func getFloatingIP(t *testing.T, ipID string) { + ip, err := floatingips.Get(base.Client, ipID).Extract() + th.AssertNoErr(t, err) + + t.Logf("Getting floating IP: ID [%s] Status [%s]", ip.ID, ip.Status) +} + +func updateFloatingIP(t *testing.T, ipID, portID string) { + t.Logf("Disassociate all ports from IP %s", ipID) + _, err := floatingips.Update(base.Client, ipID, floatingips.UpdateOpts{PortID: ""}).Extract() + th.AssertNoErr(t, err) + + t.Logf("Re-associate the port %s", portID) + _, err = floatingips.Update(base.Client, ipID, floatingips.UpdateOpts{PortID: portID}).Extract() + th.AssertNoErr(t, err) +} + +func deleteFloatingIP(t *testing.T, ipID string) { + t.Logf("Deleting IP %s", ipID) + res := floatingips.Delete(base.Client, ipID) + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/common.go new file mode 100644 index 000000000000..27dfe5f8b7ec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/common.go @@ -0,0 +1,78 @@ +package lbaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + th "github.com/rackspace/gophercloud/testhelper" +) + +func SetupTopology(t *testing.T) (string, string) { + // create network + n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created network %s", n.ID) + + // create subnet + s, err := subnets.Create(base.Client, subnets.CreateOpts{ + NetworkID: n.ID, + CIDR: "192.168.199.0/24", + IPVersion: subnets.IPv4, + Name: "tmp_subnet", + }).Extract() + th.AssertNoErr(t, err) + + t.Logf("Created subnet %s", s.ID) + + return n.ID, s.ID +} + +func DeleteTopology(t *testing.T, networkID string) { + res := networks.Delete(base.Client, networkID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted network %s", networkID) +} + +func CreatePool(t *testing.T, subnetID string) string { + p, err := pools.Create(base.Client, pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: "HTTP", + Name: "tmp_pool", + SubnetID: subnetID, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created pool %s", p.ID) + + return p.ID +} + +func DeletePool(t *testing.T, poolID string) { + res := pools.Delete(base.Client, poolID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted pool %s", poolID) +} + +func CreateMonitor(t *testing.T) string { + m, err := monitors.Create(base.Client, monitors.CreateOpts{ + Delay: 10, + Timeout: 10, + MaxRetries: 3, + Type: monitors.TypeHTTP, + ExpectedCodes: "200", + URLPath: "/login", + HTTPMethod: "GET", + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created monitor ID [%s]", m.ID) + + return m.ID +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go new file mode 100644 index 000000000000..9b60582d140d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go @@ -0,0 +1,95 @@ +// +build acceptance networking lbaas lbaasmember + +package lbaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestMembers(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // setup + networkID, subnetID := SetupTopology(t) + poolID := CreatePool(t, subnetID) + + // create member + memberID := createMember(t, poolID) + + // list members + listMembers(t) + + // update member + updateMember(t, memberID) + + // get member + getMember(t, memberID) + + // delete member + deleteMember(t, memberID) + + // teardown + DeletePool(t, poolID) + DeleteTopology(t, networkID) +} + +func createMember(t *testing.T, poolID string) string { + m, err := members.Create(base.Client, members.CreateOpts{ + Address: "192.168.199.1", + ProtocolPort: 8080, + PoolID: poolID, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created member: ID [%s] Status [%s] Weight [%d] Address [%s] Port [%d]", + m.ID, m.Status, m.Weight, m.Address, m.ProtocolPort) + + return m.ID +} + +func listMembers(t *testing.T) { + err := members.List(base.Client, members.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + memberList, err := members.ExtractMembers(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + for _, m := range memberList { + t.Logf("Listing member: ID [%s] Status [%s]", m.ID, m.Status) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updateMember(t *testing.T, memberID string) { + m, err := members.Update(base.Client, memberID, members.UpdateOpts{AdminStateUp: true}).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Updated member ID [%s]", m.ID) +} + +func getMember(t *testing.T, memberID string) { + m, err := members.Get(base.Client, memberID).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Getting member ID [%s]", m.ID) +} + +func deleteMember(t *testing.T, memberID string) { + res := members.Delete(base.Client, memberID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted member %s", memberID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go new file mode 100644 index 000000000000..9056fff671ba --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go @@ -0,0 +1,77 @@ +// +build acceptance networking lbaas lbaasmonitor + +package lbaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestMonitors(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // create monitor + monitorID := CreateMonitor(t) + + // list monitors + listMonitors(t) + + // update monitor + updateMonitor(t, monitorID) + + // get monitor + getMonitor(t, monitorID) + + // delete monitor + deleteMonitor(t, monitorID) +} + +func listMonitors(t *testing.T) { + err := monitors.List(base.Client, monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + monitorList, err := monitors.ExtractMonitors(page) + if err != nil { + t.Errorf("Failed to extract monitors: %v", err) + return false, err + } + + for _, m := range monitorList { + t.Logf("Listing monitor: ID [%s] Type [%s] Delay [%ds] Timeout [%d] Retries [%d] Status [%s]", + m.ID, m.Type, m.Delay, m.Timeout, m.MaxRetries, m.Status) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updateMonitor(t *testing.T, monitorID string) { + opts := monitors.UpdateOpts{Delay: 10, Timeout: 10, MaxRetries: 3} + m, err := monitors.Update(base.Client, monitorID, opts).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Updated monitor ID [%s]", m.ID) +} + +func getMonitor(t *testing.T, monitorID string) { + m, err := monitors.Get(base.Client, monitorID).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Getting monitor ID [%s]: URL path [%s] HTTP Method [%s] Accepted codes [%s]", + m.ID, m.URLPath, m.HTTPMethod, m.ExpectedCodes) +} + +func deleteMonitor(t *testing.T, monitorID string) { + res := monitors.Delete(base.Client, monitorID) + + th.AssertNoErr(t, res.Err) + + t.Logf("Deleted monitor %s", monitorID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go new file mode 100644 index 000000000000..f5a7df7b7515 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go @@ -0,0 +1 @@ +package lbaas diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go new file mode 100644 index 000000000000..81940649c535 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go @@ -0,0 +1,98 @@ +// +build acceptance networking lbaas lbaaspool + +package lbaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestPools(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // setup + networkID, subnetID := SetupTopology(t) + + // create pool + poolID := CreatePool(t, subnetID) + + // list pools + listPools(t) + + // update pool + updatePool(t, poolID) + + // get pool + getPool(t, poolID) + + // create monitor + monitorID := CreateMonitor(t) + + // associate health monitor + associateMonitor(t, poolID, monitorID) + + // disassociate health monitor + disassociateMonitor(t, poolID, monitorID) + + // delete pool + DeletePool(t, poolID) + + // teardown + DeleteTopology(t, networkID) +} + +func listPools(t *testing.T) { + err := pools.List(base.Client, pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + poolList, err := pools.ExtractPools(page) + if err != nil { + t.Errorf("Failed to extract pools: %v", err) + return false, err + } + + for _, p := range poolList { + t.Logf("Listing pool: ID [%s] Name [%s] Status [%s] LB algorithm [%s]", p.ID, p.Name, p.Status, p.LBMethod) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updatePool(t *testing.T, poolID string) { + opts := pools.UpdateOpts{Name: "SuperPool", LBMethod: pools.LBMethodLeastConnections} + p, err := pools.Update(base.Client, poolID, opts).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Updated pool ID [%s]", p.ID) +} + +func getPool(t *testing.T, poolID string) { + p, err := pools.Get(base.Client, poolID).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Getting pool ID [%s]", p.ID) +} + +func associateMonitor(t *testing.T, poolID, monitorID string) { + res := pools.AssociateMonitor(base.Client, poolID, monitorID) + + th.AssertNoErr(t, res.Err) + + t.Logf("Associated pool %s with monitor %s", poolID, monitorID) +} + +func disassociateMonitor(t *testing.T, poolID, monitorID string) { + res := pools.DisassociateMonitor(base.Client, poolID, monitorID) + + th.AssertNoErr(t, res.Err) + + t.Logf("Disassociated pool %s with monitor %s", poolID, monitorID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go new file mode 100644 index 000000000000..c8dff2d93ff7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go @@ -0,0 +1,101 @@ +// +build acceptance networking lbaas lbaasvip + +package lbaas + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVIPs(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // setup + networkID, subnetID := SetupTopology(t) + poolID := CreatePool(t, subnetID) + + // create VIP + VIPID := createVIP(t, subnetID, poolID) + + // list VIPs + listVIPs(t) + + // update VIP + updateVIP(t, VIPID) + + // get VIP + getVIP(t, VIPID) + + // delete VIP + deleteVIP(t, VIPID) + + // teardown + DeletePool(t, poolID) + DeleteTopology(t, networkID) +} + +func createVIP(t *testing.T, subnetID, poolID string) string { + p, err := vips.Create(base.Client, vips.CreateOpts{ + Protocol: "HTTP", + Name: "New_VIP", + AdminStateUp: vips.Up, + SubnetID: subnetID, + PoolID: poolID, + ProtocolPort: 80, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created pool %s", p.ID) + + return p.ID +} + +func listVIPs(t *testing.T) { + err := vips.List(base.Client, vips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + vipList, err := vips.ExtractVIPs(page) + if err != nil { + t.Errorf("Failed to extract VIPs: %v", err) + return false, err + } + + for _, vip := range vipList { + t.Logf("Listing VIP: ID [%s] Name [%s] Address [%s] Port [%s] Connection Limit [%d]", + vip.ID, vip.Name, vip.Address, vip.ProtocolPort, vip.ConnLimit) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func updateVIP(t *testing.T, VIPID string) { + i1000 := 1000 + _, err := vips.Update(base.Client, VIPID, vips.UpdateOpts{ConnLimit: &i1000}).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Updated VIP ID [%s]", VIPID) +} + +func getVIP(t *testing.T, VIPID string) { + vip, err := vips.Get(base.Client, VIPID).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Getting VIP ID [%s]: Status [%s]", vip.ID, vip.Status) +} + +func deleteVIP(t *testing.T, VIPID string) { + res := vips.Delete(base.Client, VIPID) + + th.AssertNoErr(t, res.Err) + + t.Logf("Deleted VIP %s", VIPID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go new file mode 100644 index 000000000000..aeec0fa756e7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/pkg.go @@ -0,0 +1 @@ +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go new file mode 100644 index 000000000000..f10c9d9bd129 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/provider_test.go @@ -0,0 +1,68 @@ +// +build acceptance networking + +package extensions + +import ( + "strconv" + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNetworkCRUDOperations(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // Create a network + n, err := networks.Create(base.Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Name, "sample_network") + th.AssertEquals(t, n.AdminStateUp, true) + networkID := n.ID + + // List networks + pager := networks.List(base.Client, networks.ListOpts{Limit: 2}) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + networkList, err := networks.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(base.Client, networkID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{}) + th.AssertEquals(t, n.Name, "sample_network") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.Shared, false) + th.AssertEquals(t, n.ID, networkID) + + // Update network + n, err = networks.Update(base.Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Name, "new_network_name") + + // Delete network + res := networks.Delete(base.Client, networkID) + th.AssertNoErr(t, res.Err) +} + +func TestCreateMultipleNetworks(t *testing.T) { + //networks.CreateMany() +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go new file mode 100644 index 000000000000..7d75292f0de6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/extensions/security_test.go @@ -0,0 +1,171 @@ +// +build acceptance networking security + +package extensions + +import ( + "testing" + + base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSecurityGroups(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // create security group + groupID := createSecGroup(t) + + // delete security group + defer deleteSecGroup(t, groupID) + + // list security group + listSecGroups(t) + + // get security group + getSecGroup(t, groupID) + + // create port with security group + networkID, portID := createPort(t, groupID) + + // teardown + defer deleteNetwork(t, networkID) + + // delete port + defer deletePort(t, portID) +} + +func TestSecurityGroupRules(t *testing.T) { + base.Setup(t) + defer base.Teardown() + + // create security group + groupID := createSecGroup(t) + + defer deleteSecGroup(t, groupID) + + // create security group rule + ruleID := createSecRule(t, groupID) + + // delete security group rule + defer deleteSecRule(t, ruleID) + + // list security group rule + listSecRules(t) + + // get security group rule + getSecRule(t, ruleID) +} + +func createSecGroup(t *testing.T) string { + sg, err := groups.Create(base.Client, groups.CreateOpts{ + Name: "new-webservers", + Description: "security group for webservers", + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created security group %s", sg.ID) + + return sg.ID +} + +func listSecGroups(t *testing.T) { + err := groups.List(base.Client, groups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + list, err := groups.ExtractGroups(page) + if err != nil { + t.Errorf("Failed to extract secgroups: %v", err) + return false, err + } + + for _, sg := range list { + t.Logf("Listing security group: ID [%s] Name [%s]", sg.ID, sg.Name) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getSecGroup(t *testing.T, id string) { + sg, err := groups.Get(base.Client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting security group: ID [%s] Name [%s] Description [%s]", sg.ID, sg.Name, sg.Description) +} + +func createPort(t *testing.T, groupID string) (string, string) { + n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created network %s", n.ID) + + opts := ports.CreateOpts{ + NetworkID: n.ID, + Name: "my_port", + SecurityGroups: []string{groupID}, + } + p, err := ports.Create(base.Client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created port %s with security group %s", p.ID, groupID) + + return n.ID, p.ID +} + +func deleteSecGroup(t *testing.T, groupID string) { + res := groups.Delete(base.Client, groupID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted security group %s", groupID) +} + +func createSecRule(t *testing.T, groupID string) string { + r, err := rules.Create(base.Client, rules.CreateOpts{ + Direction: "ingress", + PortRangeMin: 80, + EtherType: "IPv4", + PortRangeMax: 80, + Protocol: "tcp", + SecGroupID: groupID, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created security group rule %s", r.ID) + + return r.ID +} + +func listSecRules(t *testing.T) { + err := rules.List(base.Client, rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + list, err := rules.ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract sec rules: %v", err) + return false, err + } + + for _, r := range list { + t.Logf("Listing security rule: ID [%s]", r.ID) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getSecRule(t *testing.T, id string) { + r, err := rules.Get(base.Client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting security rule: ID [%s] Direction [%s] EtherType [%s] Protocol [%s]", + r.ID, r.Direction, r.EtherType, r.Protocol) +} + +func deleteSecRule(t *testing.T, id string) { + res := rules.Delete(base.Client, id) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted security rule %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/network_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/network_test.go new file mode 100644 index 000000000000..be8a3a195a4d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/network_test.go @@ -0,0 +1,68 @@ +// +build acceptance networking + +package v2 + +import ( + "strconv" + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNetworkCRUDOperations(t *testing.T) { + Setup(t) + defer Teardown() + + // Create a network + n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract() + th.AssertNoErr(t, err) + defer networks.Delete(Client, n.ID) + th.AssertEquals(t, n.Name, "sample_network") + th.AssertEquals(t, n.AdminStateUp, true) + networkID := n.ID + + // List networks + pager := networks.List(Client, networks.ListOpts{Limit: 2}) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + networkList, err := networks.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, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.Subnets, []string{}) + th.AssertEquals(t, n.Name, "sample_network") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.Shared, false) + th.AssertEquals(t, n.ID, networkID) + + // Update network + n, err = networks.Update(Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Name, "new_network_name") + + // Delete network + res := networks.Delete(Client, networkID) + th.AssertNoErr(t, res.Err) +} + +func TestCreateMultipleNetworks(t *testing.T) { + //networks.CreateMany() +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/pkg.go new file mode 100644 index 000000000000..5ec3cc8e833b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/pkg.go @@ -0,0 +1 @@ +package v2 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 new file mode 100644 index 000000000000..03e8e278428d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/port_test.go @@ -0,0 +1,117 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + 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, p.ID, portID) + + // Update port + p, err = ports.Update(Client, portID, ports.UpdateOpts{Name: "new_port_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, p.Name, "new_port_name") + + // Delete port + res := ports.Delete(Client, portID) + th.AssertNoErr(t, res.Err) +} + +func createPort(t *testing.T, networkID, subnetID string) string { + enable := false + opts := ports.CreateOpts{ + NetworkID: networkID, + Name: "my_port", + AdminStateUp: &enable, + FixedIPs: []ports.IP{ports.IP{SubnetID: subnetID}}, + } + p, err := ports.Create(Client, opts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, p.NetworkID, networkID) + th.AssertEquals(t, p.Name, "my_port") + th.AssertEquals(t, p.AdminStateUp, false) + + return p.ID +} + +func listPorts(t *testing.T) { + count := 0 + pager := ports.List(Client, ports.ListOpts{}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("--- Page ---") + + portList, err := ports.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, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract() + return res.ID, err +} + +func createSubnet(networkID string) (string, error) { + s, err := subnets.Create(Client, subnets.CreateOpts{ + NetworkID: networkID, + CIDR: "192.168.199.0/24", + IPVersion: subnets.IPv4, + Name: "my_subnet", + EnableDHCP: subnets.Down, + }).Extract() + return s.ID, err +} + +func TestPortBatchCreate(t *testing.T) { + // todo +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/subnet_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/subnet_test.go new file mode 100644 index 000000000000..097a303edeed --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/networking/v2/subnet_test.go @@ -0,0 +1,86 @@ +// +build acceptance networking + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + Setup(t) + defer Teardown() + + pager := subnets.List(Client, subnets.ListOpts{Limit: 2}) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + subnetList, err := subnets.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 TestCRUD(t *testing.T) { + Setup(t) + defer Teardown() + + // Setup network + t.Log("Setting up network") + n, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract() + th.AssertNoErr(t, err) + networkID := n.ID + defer networks.Delete(Client, networkID) + + // Create subnet + t.Log("Create subnet") + enable := false + opts := subnets.CreateOpts{ + NetworkID: networkID, + CIDR: "192.168.199.0/24", + IPVersion: subnets.IPv4, + Name: "my_subnet", + EnableDHCP: &enable, + } + s, err := subnets.Create(Client, opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, s.NetworkID, networkID) + th.AssertEquals(t, s.CIDR, "192.168.199.0/24") + th.AssertEquals(t, s.IPVersion, 4) + th.AssertEquals(t, s.Name, "my_subnet") + th.AssertEquals(t, s.EnableDHCP, false) + subnetID := s.ID + + // Get subnet + t.Log("Getting subnet") + s, err = subnets.Get(Client, subnetID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, s.ID, subnetID) + + // Update subnet + t.Log("Update subnet") + s, err = subnets.Update(Client, subnetID, subnets.UpdateOpts{Name: "new_subnet_name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, s.Name, "new_subnet_name") + + // Delete subnet + t.Log("Delete subnet") + res := subnets.Delete(Client, subnetID) + th.AssertNoErr(t, res.Err) +} + +func TestBatchCreate(t *testing.T) { + // todo +} 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 new file mode 100644 index 000000000000..24cc62b4aa08 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/accounts_test.go @@ -0,0 +1,50 @@ +// +build acceptance + +package v1 + +import ( + "strings" + "testing" + + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAccounts(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + // Update an account's metadata. + updateres := accounts.Update(client, accounts.UpdateOpts{Metadata: metadata}) + 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() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + updateres = accounts.Update(client, accounts.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, updateres.Err) + }() + + // Extract the custom metadata from the 'Get' response. + 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)] { + t.Errorf("Expected custom metadata with key: %s", k) + return + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/common.go new file mode 100644 index 000000000000..1eac681b5713 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/common.go @@ -0,0 +1,28 @@ +// +build acceptance + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +var metadata = map[string]string{"gopher": "cloud"} + +func newClient(t *testing.T) *gophercloud.ServiceClient { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + c, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + th.AssertNoErr(t, err) + return c +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go new file mode 100644 index 000000000000..8328a4fa6f11 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/containers_test.go @@ -0,0 +1,137 @@ +// +build acceptance + +package v1 + +import ( + "strings" + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +// numContainers is the number of containers to create for testing. +var numContainers = 2 + +func TestContainers(t *testing.T) { + // Create a new client to execute the HTTP requests. See common.go for newClient body. + client := newClient(t) + + // Create a slice of random container names. + cNames := make([]string, numContainers) + for i := 0; i < numContainers; i++ { + cNames[i] = tools.RandomString("gophercloud-test-container-", 8) + } + + // Create numContainers containers. + for i := 0; i < len(cNames); i++ { + res := containers.Create(client, cNames[i], nil) + th.AssertNoErr(t, res.Err) + } + // Delete the numContainers containers after function completion. + defer func() { + for i := 0; i < len(cNames); i++ { + res := containers.Delete(client, cNames[i]) + th.AssertNoErr(t, res.Err) + } + }() + + // List the numContainer names that were just created. To just list those, + // the 'prefix' parameter is used. + err := containers.List(client, &containers.ListOpts{Full: true, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) { + containerList, err := containers.ExtractInfo(page) + th.AssertNoErr(t, err) + + for _, n := range containerList { + t.Logf("Container: Name [%s] Count [%d] Bytes [%d]", + n.Name, n.Count, n.Bytes) + } + + return true, nil + }) + th.AssertNoErr(t, err) + + // List the info for the numContainer containers that were created. + err = containers.List(client, &containers.ListOpts{Full: false, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) { + containerList, err := containers.ExtractNames(page) + th.AssertNoErr(t, err) + for _, n := range containerList { + t.Logf("Container: Name [%s]", n) + } + + return true, nil + }) + th.AssertNoErr(t, err) + + // Update one of the numContainer container metadata. + updateres := containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: metadata}) + th.AssertNoErr(t, updateres.Err) + // After the tests are done, delete the metadata that was set. + defer func() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + res := containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, res.Err) + }() + + // Retrieve a container's metadata. + cm, err := containers.Get(client, cNames[0]).ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if cm[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + } + } +} + +func TestListAllContainers(t *testing.T) { + // Create a new client to execute the HTTP requests. See common.go for newClient body. + client := newClient(t) + + numContainers := 20 + + // Create a slice of random container names. + cNames := make([]string, numContainers) + for i := 0; i < numContainers; i++ { + cNames[i] = tools.RandomString("gophercloud-test-container-", 8) + } + + // Create numContainers containers. + for i := 0; i < len(cNames); i++ { + res := containers.Create(client, cNames[i], nil) + th.AssertNoErr(t, res.Err) + } + // Delete the numContainers containers after function completion. + defer func() { + for i := 0; i < len(cNames); i++ { + res := containers.Delete(client, cNames[i]) + th.AssertNoErr(t, res.Err) + } + }() + + // List all the numContainer names that were just created. To just list those, + // the 'prefix' parameter is used. + allPages, err := containers.List(client, &containers.ListOpts{Full: true, Limit: 5, Prefix: "gophercloud-test-container-"}).AllPages() + th.AssertNoErr(t, err) + containerInfoList, err := containers.ExtractInfo(allPages) + th.AssertNoErr(t, err) + for _, n := range containerInfoList { + t.Logf("Container: Name [%s] Count [%d] Bytes [%d]", + n.Name, n.Count, n.Bytes) + } + th.AssertEquals(t, numContainers, len(containerInfoList)) + + // List the info for all the numContainer containers that were created. + allPages, err = containers.List(client, &containers.ListOpts{Full: false, Limit: 2, Prefix: "gophercloud-test-container-"}).AllPages() + th.AssertNoErr(t, err) + containerNamesList, err := containers.ExtractNames(allPages) + th.AssertNoErr(t, err) + for _, n := range containerNamesList { + t.Logf("Container: Name [%s]", n) + } + th.AssertEquals(t, numContainers, len(containerNamesList)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go new file mode 100644 index 000000000000..a8de338c3dd6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/objectstorage/v1/objects_test.go @@ -0,0 +1,119 @@ +// +build acceptance + +package v1 + +import ( + "bytes" + "strings" + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +// numObjects is the number of objects to create for testing. +var numObjects = 2 + +func TestObjects(t *testing.T) { + // Create a provider client for executing the HTTP request. + // See common.go for more information. + client := newClient(t) + + // Make a slice of length numObjects to hold the random object names. + oNames := make([]string, numObjects) + for i := 0; i < len(oNames); i++ { + oNames[i] = tools.RandomString("test-object-", 8) + } + + // Create a container to hold the test objects. + cName := tools.RandomString("test-container-", 8) + header, err := containers.Create(client, cName, nil).ExtractHeader() + th.AssertNoErr(t, err) + t.Logf("Create object headers: %+v\n", header) + + // Defer deletion of the container until after testing. + defer func() { + res := containers.Delete(client, cName) + th.AssertNoErr(t, res.Err) + }() + + // Create a slice of buffers to hold the test object content. + oContents := make([]*bytes.Buffer, numObjects) + for i := 0; i < numObjects; i++ { + oContents[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10))) + res := objects.Create(client, cName, oNames[i], oContents[i], nil) + th.AssertNoErr(t, res.Err) + } + // Delete the objects after testing. + defer func() { + for i := 0; i < numObjects; i++ { + res := objects.Delete(client, cName, oNames[i], nil) + th.AssertNoErr(t, res.Err) + } + }() + + ons := make([]string, 0, len(oNames)) + err = objects.List(client, cName, &objects.ListOpts{Full: false, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) { + names, err := objects.ExtractNames(page) + th.AssertNoErr(t, err) + ons = append(ons, names...) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, len(ons), len(oNames)) + + ois := make([]objects.Object, 0, len(oNames)) + err = objects.List(client, cName, &objects.ListOpts{Full: true, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) { + info, err := objects.ExtractInfo(page) + th.AssertNoErr(t, err) + + ois = append(ois, info...) + + return true, nil + }) + th.AssertNoErr(t, err) + th.AssertEquals(t, len(ois), len(oNames)) + + // Copy the contents of one object to another. + copyres := objects.Copy(client, cName, oNames[0], &objects.CopyOpts{Destination: cName + "/" + oNames[1]}) + th.AssertNoErr(t, copyres.Err) + + // Download one of the objects that was created above. + o1Content, err := objects.Download(client, cName, oNames[0], nil).ExtractContent() + th.AssertNoErr(t, err) + + // Download the another object that was create above. + o2Content, err := objects.Download(client, cName, oNames[1], nil).ExtractContent() + th.AssertNoErr(t, err) + + // Compare the two object's contents to test that the copy worked. + th.AssertEquals(t, string(o2Content), string(o1Content)) + + // Update an object's metadata. + updateres := objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: metadata}) + th.AssertNoErr(t, updateres.Err) + + // Delete the object's metadata after testing. + defer func() { + tempMap := make(map[string]string) + for k := range metadata { + tempMap[k] = "" + } + res := objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: tempMap}) + th.AssertNoErr(t, res.Err) + }() + + // Retrieve an object's metadata. + om, err := objects.Get(client, cName, oNames[0], nil).ExtractMetadata() + th.AssertNoErr(t, err) + for k := range metadata { + if om[k] != metadata[strings.Title(k)] { + t.Errorf("Expected custom metadata with key: %s", k) + return + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/buildinfo_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/buildinfo_test.go new file mode 100644 index 000000000000..05a5e1d67ec3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/buildinfo_test.go @@ -0,0 +1,20 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestBuildInfo(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + bi, err := buildinfo.Get(client).Extract() + th.AssertNoErr(t, err) + t.Logf("retrieved build info: %+v\n", bi) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/common.go new file mode 100644 index 000000000000..2c28dcbcc986 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/common.go @@ -0,0 +1,44 @@ +// +build acceptance + +package v1 + +import ( + "fmt" + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + th "github.com/rackspace/gophercloud/testhelper" +) + +var template = fmt.Sprintf(` +{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": {}, + "resources": { + "hello_world": { + "type":"OS::Nova::Server", + "properties": { + "flavor": "%s", + "image": "%s", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } +}`, os.Getenv("OS_FLAVOR_ID"), os.Getenv("OS_IMAGE_ID")) + +func newClient(t *testing.T) *gophercloud.ServiceClient { + ao, err := openstack.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := openstack.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + c, err := openstack.NewOrchestrationV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + th.AssertNoErr(t, err) + return c +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/hello-compute.json b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/hello-compute.json new file mode 100644 index 000000000000..11cfc8053428 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/hello-compute.json @@ -0,0 +1,13 @@ +{ + "heat_template_version": "2013-05-23", + "resources": { + "compute_instance": { + "type": "OS::Nova::Server", + "properties": { + "flavor": "m1.small", + "image": "cirros-0.3.2-x86_64-disk", + "name": "Single Compute Instance" + } + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stackevents_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stackevents_test.go new file mode 100644 index 000000000000..e356c86aa9db --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stackevents_test.go @@ -0,0 +1,68 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents" + "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestStackEvents(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName := "postman_stack_2" + resourceName := "hello_world" + var eventID string + + createOpts := stacks.CreateOpts{ + Name: stackName, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + err = stackevents.List(client, stackName, stack.ID, nil).EachPage(func(page pagination.Page) (bool, error) { + events, err := stackevents.ExtractEvents(page) + th.AssertNoErr(t, err) + t.Logf("listed events: %+v\n", events) + eventID = events[0].ID + return false, nil + }) + th.AssertNoErr(t, err) + + err = stackevents.ListResourceEvents(client, stackName, stack.ID, resourceName, nil).EachPage(func(page pagination.Page) (bool, error) { + resourceEvents, err := stackevents.ExtractEvents(page) + th.AssertNoErr(t, err) + t.Logf("listed resource events: %+v\n", resourceEvents) + return false, nil + }) + th.AssertNoErr(t, err) + + event, err := stackevents.Get(client, stackName, stack.ID, resourceName, eventID).Extract() + th.AssertNoErr(t, err) + t.Logf("retrieved event: %+v\n", event) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stackresources_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stackresources_test.go new file mode 100644 index 000000000000..b614f1cef5f2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stackresources_test.go @@ -0,0 +1,62 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources" + "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestStackResources(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName := "postman_stack_2" + + createOpts := stacks.CreateOpts{ + Name: stackName, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + resourceName := "hello_world" + resource, err := stackresources.Get(client, stackName, stack.ID, resourceName).Extract() + th.AssertNoErr(t, err) + t.Logf("Got stack resource: %+v\n", resource) + + metadata, err := stackresources.Metadata(client, stackName, stack.ID, resourceName).Extract() + th.AssertNoErr(t, err) + t.Logf("Got stack resource metadata: %+v\n", metadata) + + err = stackresources.List(client, stackName, stack.ID, stackresources.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + resources, err := stackresources.ExtractResources(page) + th.AssertNoErr(t, err) + t.Logf("resources: %+v\n", resources) + return false, nil + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stacks_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stacks_test.go new file mode 100644 index 000000000000..01e76d61403b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stacks_test.go @@ -0,0 +1,81 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestStacks(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName1 := "gophercloud-test-stack-2" + createOpts := stacks.CreateOpts{ + Name: stackName1, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName1, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName1) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + updateOpts := stacks.UpdateOpts{ + Template: template, + Timeout: 20, + } + err = stacks.Update(client, stackName1, stack.ID, updateOpts).ExtractErr() + th.AssertNoErr(t, err) + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "UPDATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + t.Logf("Updated stack") + + err = stacks.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + stackList, err := stacks.ExtractStacks(page) + th.AssertNoErr(t, err) + + t.Logf("Got stack list: %+v\n", stackList) + + return true, nil + }) + th.AssertNoErr(t, err) + + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Got stack: %+v\n", getStack) + + abandonedStack, err := stacks.Abandon(client, stackName1, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Abandonded stack %+v\n", abandonedStack) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stacktemplates_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stacktemplates_test.go new file mode 100644 index 000000000000..14d8f4437ae2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/orchestration/v1/stacktemplates_test.go @@ -0,0 +1,77 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" + "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestStackTemplates(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName := "postman_stack_2" + + createOpts := stacks.CreateOpts{ + Name: stackName, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + tmpl, err := stacktemplates.Get(client, stackName, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("retrieved template: %+v\n", tmpl) + + validateOpts := stacktemplates.ValidateOpts{ + Template: map[string]interface{}{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": map[string]interface{}{ + "flavor": map[string]interface{}{ + "default": "m1.tiny", + "type": "string", + }, + }, + "resources": map[string]interface{}{ + "hello_world": map[string]interface{}{ + "type": "OS::Nova::Server", + "properties": map[string]interface{}{ + "key_name": "heat_key", + "flavor": map[string]interface{}{ + "get_param": "flavor", + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", + }, + }, + }, + }, + } + validatedTemplate, err := stacktemplates.Validate(client, validateOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("validated template: %+v\n", validatedTemplate) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/pkg.go new file mode 100644 index 000000000000..3a8ecdb100b0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/openstack/pkg.go @@ -0,0 +1,4 @@ +// +build acceptance + +package openstack + diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/common.go new file mode 100644 index 000000000000..e9fdd9920596 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/common.go @@ -0,0 +1,38 @@ +// +build acceptance + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newClient() (*gophercloud.ServiceClient, error) { + opts, err := rackspace.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + opts = tools.OnlyRS(opts) + region := os.Getenv("RS_REGION") + + provider, err := rackspace.AuthenticatedClient(opts) + if err != nil { + return nil, err + } + + return rackspace.NewBlockStorageV1(provider, gophercloud.EndpointOpts{ + Region: region, + }) +} + +func setup(t *testing.T) *gophercloud.ServiceClient { + client, err := newClient() + th.AssertNoErr(t, err) + + return client +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/snapshot_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/snapshot_test.go new file mode 100644 index 000000000000..25b2cfeeeb22 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/snapshot_test.go @@ -0,0 +1,82 @@ +// +build acceptance blockstorage snapshots + +package v1 + +import ( + "testing" + "time" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSnapshots(t *testing.T) { + client := setup(t) + volID := testVolumeCreate(t, client) + + t.Log("Creating snapshots") + s := testSnapshotCreate(t, client, volID) + id := s.ID + + t.Log("Listing snapshots") + testSnapshotList(t, client) + + t.Logf("Getting snapshot %s", id) + testSnapshotGet(t, client, id) + + t.Logf("Updating snapshot %s", id) + testSnapshotUpdate(t, client, id) + + t.Logf("Deleting snapshot %s", id) + testSnapshotDelete(t, client, id) + s.WaitUntilDeleted(client, -1) + + t.Logf("Deleting volume %s", volID) + testVolumeDelete(t, client, volID) +} + +func testSnapshotCreate(t *testing.T, client *gophercloud.ServiceClient, volID string) *snapshots.Snapshot { + opts := snapshots.CreateOpts{VolumeID: volID, Name: "snapshot-001"} + s, err := snapshots.Create(client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created snapshot %s", s.ID) + + t.Logf("Waiting for new snapshot to become available...") + start := time.Now().Second() + s.WaitUntilComplete(client, -1) + t.Logf("Snapshot completed after %ds", time.Now().Second()-start) + + return s +} + +func testSnapshotList(t *testing.T, client *gophercloud.ServiceClient) { + snapshots.List(client).EachPage(func(page pagination.Page) (bool, error) { + sList, err := snapshots.ExtractSnapshots(page) + th.AssertNoErr(t, err) + + for _, s := range sList { + t.Logf("Snapshot: ID [%s] Name [%s] Volume ID [%s] Progress [%s] Created [%s]", + s.ID, s.Name, s.VolumeID, s.Progress, s.CreatedAt) + } + + return true, nil + }) +} + +func testSnapshotGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + _, err := snapshots.Get(client, id).Extract() + th.AssertNoErr(t, err) +} + +func testSnapshotUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) { + _, err := snapshots.Update(client, id, snapshots.UpdateOpts{Name: "new_name"}).Extract() + th.AssertNoErr(t, err) +} + +func testSnapshotDelete(t *testing.T, client *gophercloud.ServiceClient, id string) { + res := snapshots.Delete(client, id) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted snapshot %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_test.go new file mode 100644 index 000000000000..f86f9adedd3d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_test.go @@ -0,0 +1,71 @@ +// +build acceptance blockstorage volumes + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVolumes(t *testing.T) { + client := setup(t) + + t.Logf("Listing volumes") + testVolumeList(t, client) + + t.Logf("Creating volume") + volumeID := testVolumeCreate(t, client) + + t.Logf("Getting volume %s", volumeID) + testVolumeGet(t, client, volumeID) + + t.Logf("Updating volume %s", volumeID) + testVolumeUpdate(t, client, volumeID) + + t.Logf("Deleting volume %s", volumeID) + testVolumeDelete(t, client, volumeID) +} + +func testVolumeList(t *testing.T, client *gophercloud.ServiceClient) { + volumes.List(client).EachPage(func(page pagination.Page) (bool, error) { + vList, err := volumes.ExtractVolumes(page) + th.AssertNoErr(t, err) + + for _, v := range vList { + t.Logf("Volume: ID [%s] Name [%s] Type [%s] Created [%s]", v.ID, v.Name, + v.VolumeType, v.CreatedAt) + } + + return true, nil + }) +} + +func testVolumeCreate(t *testing.T, client *gophercloud.ServiceClient) string { + vol, err := volumes.Create(client, os.CreateOpts{Size: 75}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created volume: ID [%s] Size [%s]", vol.ID, vol.Size) + return vol.ID +} + +func testVolumeGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + vol, err := volumes.Get(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Created volume: ID [%s] Size [%s]", vol.ID, vol.Size) +} + +func testVolumeUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) { + vol, err := volumes.Update(client, id, volumes.UpdateOpts{Name: "new_name"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created volume: ID [%s] Name [%s]", vol.ID, vol.Name) +} + +func testVolumeDelete(t *testing.T, client *gophercloud.ServiceClient, id string) { + res := volumes.Delete(client, id) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted volume %s", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_type_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_type_test.go new file mode 100644 index 000000000000..716f2b9fd5b5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/blockstorage/v1/volume_type_test.go @@ -0,0 +1,46 @@ +// +build acceptance blockstorage volumetypes + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAll(t *testing.T) { + client := setup(t) + + t.Logf("Listing volume types") + id := testList(t, client) + + t.Logf("Getting volume type %s", id) + testGet(t, client, id) +} + +func testList(t *testing.T, client *gophercloud.ServiceClient) string { + var lastID string + + volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) { + typeList, err := volumetypes.ExtractVolumeTypes(page) + th.AssertNoErr(t, err) + + for _, vt := range typeList { + t.Logf("Volume type: ID [%s] Name [%s]", vt.ID, vt.Name) + lastID = vt.ID + } + + return true, nil + }) + + return lastID +} + +func testGet(t *testing.T, client *gophercloud.ServiceClient, id string) { + vt, err := volumetypes.Get(client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Volume: ID [%s] Name [%s]", vt.ID, vt.Name) +} 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 000000000000..135f5b330afd --- /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 000000000000..2333ca77bff3 --- /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 000000000000..f26cff0140cd --- /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 000000000000..c19c241c3649 --- /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 000000000000..c32bf253da2e --- /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/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/client_test.go new file mode 100644 index 000000000000..61214c047a99 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/client_test.go @@ -0,0 +1,28 @@ +// +build acceptance + +package rackspace + +import ( + "testing" + + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticatedClient(t *testing.T) { + // Obtain credentials from the environment. + ao, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := rackspace.AuthenticatedClient(tools.OnlyRS(ao)) + if err != nil { + t.Fatalf("Unable to authenticate: %v", err) + } + + if client.TokenID == "" { + t.Errorf("No token ID assigned to the client") + } + + t.Logf("Client successfully acquired a token: %v", client.TokenID) +} 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 new file mode 100644 index 000000000000..d7e6aa712c11 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/bootfromvolume_test.go @@ -0,0 +1,49 @@ +// +build acceptance + +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" +) + +func TestBootFromVolume(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + name := tools.RandomString("Gophercloud-", 8) + t.Logf("Creating server [%s].", name) + + bd := []osBFV.BlockDevice{ + osBFV.BlockDevice{ + UUID: options.imageID, + SourceType: osBFV.Image, + VolumeSize: 10, + }, + } + + server, err := bootfromvolume.Create(client, servers.CreateOpts{ + Name: name, + FlavorRef: "performance1-1", + BlockDevice: bd, + }).Extract() + th.AssertNoErr(t, err) + t.Logf("Created server: %+v\n", server) + 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/compute_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/compute_test.go new file mode 100644 index 000000000000..3ca6dc9b6c8d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/compute_test.go @@ -0,0 +1,60 @@ +// +build acceptance + +package v2 + +import ( + "errors" + "os" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" +) + +func newClient() (*gophercloud.ServiceClient, error) { + // Obtain credentials from the environment. + options, err := rackspace.AuthOptionsFromEnv() + if err != nil { + return nil, err + } + options = tools.OnlyRS(options) + region := os.Getenv("RS_REGION") + + if options.Username == "" { + return nil, errors.New("Please provide a Rackspace username as RS_USERNAME.") + } + if options.APIKey == "" { + return nil, errors.New("Please provide a Rackspace API key as RS_API_KEY.") + } + if region == "" { + return nil, errors.New("Please provide a Rackspace region as RS_REGION.") + } + + client, err := rackspace.AuthenticatedClient(options) + if err != nil { + return nil, err + } + + return rackspace.NewComputeV2(client, gophercloud.EndpointOpts{ + Region: region, + }) +} + +type serverOpts struct { + imageID string + flavorID string +} + +func optionsFromEnv() (*serverOpts, error) { + options := &serverOpts{ + imageID: os.Getenv("RS_IMAGE_ID"), + flavorID: os.Getenv("RS_FLAVOR_ID"), + } + if options.imageID == "" { + return nil, errors.New("Please provide a valid Rackspace image ID as RS_IMAGE_ID") + } + if options.flavorID == "" { + return nil, errors.New("Please provide a valid Rackspace flavor ID as RS_FLAVOR_ID") + } + return options, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/flavors_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/flavors_test.go new file mode 100644 index 000000000000..4618ecc8a9b1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/flavors_test.go @@ -0,0 +1,61 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/flavors" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListFlavors(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + count := 0 + err = flavors.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("-- Page %0d --", count) + + fs, err := flavors.ExtractFlavors(page) + th.AssertNoErr(t, err) + + for i, flavor := range fs { + t.Logf("[%02d] id=[%s]", i, flavor.ID) + t.Logf(" name=[%s]", flavor.Name) + t.Logf(" disk=[%d]", flavor.Disk) + t.Logf(" RAM=[%d]", flavor.RAM) + t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor) + t.Logf(" swap=[%d]", flavor.Swap) + t.Logf(" VCPUs=[%d]", flavor.VCPUs) + } + + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No flavors listed!") + } +} + +func TestGetFlavor(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + flavor, err := flavors.Get(client, options.flavorID).Extract() + th.AssertNoErr(t, err) + + t.Logf("Requested flavor:") + t.Logf(" id=[%s]", flavor.ID) + t.Logf(" name=[%s]", flavor.Name) + t.Logf(" disk=[%d]", flavor.Disk) + t.Logf(" RAM=[%d]", flavor.RAM) + t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor) + t.Logf(" swap=[%d]", flavor.Swap) + t.Logf(" VCPUs=[%d]", flavor.VCPUs) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/images_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/images_test.go new file mode 100644 index 000000000000..5e36c2e4545e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/images_test.go @@ -0,0 +1,63 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/images" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestListImages(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + count := 0 + err = images.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("-- Page %02d --", count) + + is, err := images.ExtractImages(page) + th.AssertNoErr(t, err) + + for i, image := range is { + t.Logf("[%02d] id=[%s]", i, image.ID) + t.Logf(" name=[%s]", image.Name) + t.Logf(" created=[%s]", image.Created) + t.Logf(" updated=[%s]", image.Updated) + t.Logf(" min disk=[%d]", image.MinDisk) + t.Logf(" min RAM=[%d]", image.MinRAM) + t.Logf(" progress=[%d]", image.Progress) + t.Logf(" status=[%s]", image.Status) + } + + return true, nil + }) + th.AssertNoErr(t, err) + if count < 1 { + t.Errorf("Expected at least one page of images.") + } +} + +func TestGetImage(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + image, err := images.Get(client, options.imageID).Extract() + th.AssertNoErr(t, err) + + t.Logf("Requested image:") + t.Logf(" id=[%s]", image.ID) + t.Logf(" name=[%s]", image.Name) + t.Logf(" created=[%s]", image.Created) + t.Logf(" updated=[%s]", image.Updated) + t.Logf(" min disk=[%d]", image.MinDisk) + t.Logf(" min RAM=[%d]", image.MinRAM) + t.Logf(" progress=[%d]", image.Progress) + t.Logf(" status=[%s]", image.Status) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/keypairs_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/keypairs_test.go new file mode 100644 index 000000000000..9bd6eb428481 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/keypairs_test.go @@ -0,0 +1,87 @@ +// +build acceptance rackspace + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs" + th "github.com/rackspace/gophercloud/testhelper" +) + +func deleteKeyPair(t *testing.T, client *gophercloud.ServiceClient, name string) { + err := keypairs.Delete(client, name).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Successfully deleted key [%s].", name) +} + +func TestCreateKeyPair(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + name := tools.RandomString("createdkey-", 8) + k, err := keypairs.Create(client, os.CreateOpts{Name: name}).Extract() + th.AssertNoErr(t, err) + defer deleteKeyPair(t, client, name) + + t.Logf("Created a new keypair:") + t.Logf(" name=[%s]", k.Name) + t.Logf(" fingerprint=[%s]", k.Fingerprint) + t.Logf(" publickey=[%s]", tools.Elide(k.PublicKey)) + t.Logf(" privatekey=[%s]", tools.Elide(k.PrivateKey)) + t.Logf(" userid=[%s]", k.UserID) +} + +func TestImportKeyPair(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + name := tools.RandomString("importedkey-", 8) + pubkey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlIQ3r+zd97kb9Hzmujd3V6pbO53eb3Go4q2E8iqVGWQfZTrFdL9KACJnqJIm9HmncfRkUTxE37hqeGCCv8uD+ZPmPiZG2E60OX1mGDjbbzAyReRwYWXgXHopggZTLak5k4mwZYaxwaufbVBDRn847e01lZnaXaszEToLM37NLw+uz29sl3TwYy2R0RGHPwPc160aWmdLjSyd1Nd4c9pvvOP/EoEuBjIC6NJJwg2Rvg9sjjx9jYj0QUgc8CqKLN25oMZ69kNJzlFylKRUoeeVr89txlR59yehJWk6Uw6lYFTdJmcmQOFVAJ12RMmS1hLWCM8UzAgtw+EDa0eqBxBDl smash@winter" + + k, err := keypairs.Create(client, os.CreateOpts{ + Name: name, + PublicKey: pubkey, + }).Extract() + th.AssertNoErr(t, err) + defer deleteKeyPair(t, client, name) + + th.CheckEquals(t, pubkey, k.PublicKey) + th.CheckEquals(t, "", k.PrivateKey) + + t.Logf("Imported an existing keypair:") + t.Logf(" name=[%s]", k.Name) + t.Logf(" fingerprint=[%s]", k.Fingerprint) + t.Logf(" publickey=[%s]", tools.Elide(k.PublicKey)) + t.Logf(" privatekey=[%s]", tools.Elide(k.PrivateKey)) + t.Logf(" userid=[%s]", k.UserID) +} + +func TestListKeyPairs(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + count := 0 + err = keypairs.List(client).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("--- %02d ---", count) + + ks, err := keypairs.ExtractKeyPairs(page) + th.AssertNoErr(t, err) + + for i, keypair := range ks { + t.Logf("[%02d] name=[%s]", i, keypair.Name) + t.Logf(" fingerprint=[%s]", keypair.Fingerprint) + t.Logf(" publickey=[%s]", tools.Elide(keypair.PublicKey)) + t.Logf(" privatekey=[%s]", tools.Elide(keypair.PrivateKey)) + t.Logf(" userid=[%s]", keypair.UserID) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/networks_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/networks_test.go new file mode 100644 index 000000000000..e8fc4d37dfc0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/networks_test.go @@ -0,0 +1,53 @@ +// +build acceptance rackspace + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/networks" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestNetworks(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + // Create a network + n, err := networks.Create(client, networks.CreateOpts{Label: "sample_network", CIDR: "172.20.0.0/24"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created network: %+v\n", n) + defer networks.Delete(client, n.ID) + th.AssertEquals(t, n.Label, "sample_network") + th.AssertEquals(t, n.CIDR, "172.20.0.0/24") + networkID := n.ID + + // List networks + pager := networks.List(client) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + networkList, err := networks.ExtractNetworks(page) + th.AssertNoErr(t, err) + + for _, n := range networkList { + t.Logf("Network: ID [%s] Label [%s] CIDR [%s]", + n.ID, n.Label, n.CIDR) + } + + 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() + t.Logf("Retrieved Network: %+v\n", n) + th.AssertNoErr(t, err) + th.AssertEquals(t, n.CIDR, "172.20.0.0/24") + th.AssertEquals(t, n.Label, "sample_network") + th.AssertEquals(t, n.ID, networkID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/pkg.go new file mode 100644 index 000000000000..5ec3cc8e833b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/pkg.go @@ -0,0 +1 @@ +package v2 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 new file mode 100644 index 000000000000..a8b5937b6ee8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/servers_test.go @@ -0,0 +1,217 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + oskey "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs" + "github.com/rackspace/gophercloud/rackspace/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func createServerKeyPair(t *testing.T, client *gophercloud.ServiceClient) *oskey.KeyPair { + name := tools.RandomString("importedkey-", 8) + pubkey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlIQ3r+zd97kb9Hzmujd3V6pbO53eb3Go4q2E8iqVGWQfZTrFdL9KACJnqJIm9HmncfRkUTxE37hqeGCCv8uD+ZPmPiZG2E60OX1mGDjbbzAyReRwYWXgXHopggZTLak5k4mwZYaxwaufbVBDRn847e01lZnaXaszEToLM37NLw+uz29sl3TwYy2R0RGHPwPc160aWmdLjSyd1Nd4c9pvvOP/EoEuBjIC6NJJwg2Rvg9sjjx9jYj0QUgc8CqKLN25oMZ69kNJzlFylKRUoeeVr89txlR59yehJWk6Uw6lYFTdJmcmQOFVAJ12RMmS1hLWCM8UzAgtw+EDa0eqBxBDl smash@winter" + + k, err := keypairs.Create(client, oskey.CreateOpts{ + Name: name, + PublicKey: pubkey, + }).Extract() + th.AssertNoErr(t, err) + + return k +} + +func createServer(t *testing.T, client *gophercloud.ServiceClient, keyName string) *os.Server { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + 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 != "" { + opts.KeyPair = keyName + } + + t.Logf("Creating server [%s].", name) + s, err := servers.Create(client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Creating server.") + + err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + t.Logf("Server created successfully.") + + th.CheckEquals(t, pwd, s.AdminPass) + + return s +} + +func logServer(t *testing.T, server *os.Server, index int) { + if index == -1 { + t.Logf(" id=[%s]", server.ID) + } else { + t.Logf("[%02d] id=[%s]", index, server.ID) + } + t.Logf(" name=[%s]", server.Name) + t.Logf(" tenant ID=[%s]", server.TenantID) + t.Logf(" user ID=[%s]", server.UserID) + t.Logf(" updated=[%s]", server.Updated) + t.Logf(" created=[%s]", server.Created) + t.Logf(" host ID=[%s]", server.HostID) + t.Logf(" access IPv4=[%s]", server.AccessIPv4) + t.Logf(" access IPv6=[%s]", server.AccessIPv6) + t.Logf(" image=[%v]", server.Image) + t.Logf(" flavor=[%v]", server.Flavor) + t.Logf(" addresses=[%v]", server.Addresses) + t.Logf(" metadata=[%v]", server.Metadata) + t.Logf(" links=[%v]", server.Links) + t.Logf(" keyname=[%s]", server.KeyName) + t.Logf(" admin password=[%s]", server.AdminPass) + t.Logf(" status=[%s]", server.Status) + t.Logf(" progress=[%d]", server.Progress) +} + +func getServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Get") + + details, err := servers.Get(client, server.ID).Extract() + th.AssertNoErr(t, err) + 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") + + count := 0 + err := servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + t.Logf("--- Page %02d ---", count) + + s, err := servers.ExtractServers(page) + th.AssertNoErr(t, err) + for index, server := range s { + logServer(t, &server, index) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} + +func changeAdminPassword(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.ChangeAdminPassword") + + original := server.AdminPass + + t.Logf("Changing server password.") + err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).ExtractErr() + th.AssertNoErr(t, err) + + err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + t.Logf("Password changed successfully.") +} + +func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Reboot") + + err := servers.Reboot(client, server.ID, os.HardReboot).ExtractErr() + th.AssertNoErr(t, err) + + err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + + t.Logf("Server successfully rebooted.") +} + +func rebuildServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Rebuild") + + options, err := optionsFromEnv() + th.AssertNoErr(t, err) + + opts := servers.RebuildOpts{ + Name: tools.RandomString("RenamedGopher", 16), + AdminPass: tools.MakeNewPassword(server.AdminPass), + ImageID: options.imageID, + DiskConfig: diskconfig.Manual, + } + after, err := servers.Rebuild(client, server.ID, opts).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, after.ID, server.ID) + + err = servers.WaitForStatus(client, after.ID, "ACTIVE", 300) + th.AssertNoErr(t, err) + + t.Logf("Server successfully rebuilt.") + logServer(t, after, -1) +} + +func deleteServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) { + t.Logf("> servers.Delete") + + res := servers.Delete(client, server.ID) + th.AssertNoErr(t, res.Err) + + t.Logf("Server deleted successfully.") +} + +func deleteServerKeyPair(t *testing.T, client *gophercloud.ServiceClient, k *oskey.KeyPair) { + t.Logf("> keypairs.Delete") + + err := keypairs.Delete(client, k.Name).ExtractErr() + th.AssertNoErr(t, err) + + t.Logf("Keypair deleted successfully.") +} + +func TestServerOperations(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + kp := createServerKeyPair(t, client) + defer deleteServerKeyPair(t, client, kp) + + server := createServer(t, client, kp.Name) + defer deleteServer(t, client, server) + + getServer(t, client, server) + updateServer(t, client, server) + listServers(t, client) + changeAdminPassword(t, client, server) + rebootServer(t, client, server) + rebuildServer(t, client, server) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/virtualinterfaces_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/virtualinterfaces_test.go new file mode 100644 index 000000000000..39475e176e27 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/virtualinterfaces_test.go @@ -0,0 +1,53 @@ +// +build acceptance rackspace + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/compute/v2/networks" + "github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestVirtualInterfaces(t *testing.T) { + client, err := newClient() + th.AssertNoErr(t, err) + + // Create a server + server := createServer(t, client, "") + t.Logf("Created Server: %v\n", server) + defer deleteServer(t, client, server) + serverID := server.ID + + // Create a network + n, err := networks.Create(client, networks.CreateOpts{Label: "sample_network", CIDR: "172.20.0.0/24"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created Network: %v\n", n) + defer networks.Delete(client, n.ID) + networkID := n.ID + + // Create a virtual interface + vi, err := virtualinterfaces.Create(client, serverID, networkID).Extract() + th.AssertNoErr(t, err) + t.Logf("Created virtual interface: %+v\n", vi) + defer virtualinterfaces.Delete(client, serverID, vi.ID) + + // List virtual interfaces + pager := virtualinterfaces.List(client, serverID) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + virtualinterfacesList, err := virtualinterfaces.ExtractVirtualInterfaces(page) + th.AssertNoErr(t, err) + + for _, vi := range virtualinterfacesList { + t.Logf("Virtual Interface: ID [%s] MAC Address [%s] IP Addresses [%v]", + vi.ID, vi.MACAddress, vi.IPAddresses) + } + + return true, nil + }) + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/volumeattach_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/volumeattach_test.go new file mode 100644 index 000000000000..9848e2eba59a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/compute/v2/volumeattach_test.go @@ -0,0 +1,130 @@ +// +build acceptance compute servers + +package v2 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/openstack" + osVolumes "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + osVolumeAttach "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" + osServers "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/rackspace" + "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/rackspace/compute/v2/servers" + "github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach" + th "github.com/rackspace/gophercloud/testhelper" +) + +func newBlockClient(t *testing.T) (*gophercloud.ServiceClient, error) { + ao, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + + client, err := rackspace.AuthenticatedClient(ao) + th.AssertNoErr(t, err) + + return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("RS_REGION_NAME"), + }) +} + +func createVAServer(t *testing.T, computeClient *gophercloud.ServiceClient, choices *serverOpts) (*osServers.Server, error) { + if testing.Short() { + t.Skip("Skipping test that requires server creation in short mode.") + } + + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create server: %s\n", name) + + pwd := tools.MakeNewPassword("") + + server, err := servers.Create(computeClient, osServers.CreateOpts{ + Name: name, + FlavorRef: choices.flavorID, + ImageRef: choices.imageID, + AdminPass: pwd, + }).Extract() + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + + th.AssertEquals(t, pwd, server.AdminPass) + + return server, err +} + +func createVAVolume(t *testing.T, blockClient *gophercloud.ServiceClient) (*volumes.Volume, error) { + volume, err := volumes.Create(blockClient, &osVolumes.CreateOpts{ + Size: 80, + Name: "gophercloud-test-volume", + }).Extract() + th.AssertNoErr(t, err) + defer func() { + err = osVolumes.WaitForStatus(blockClient, volume.ID, "available", 60) + th.AssertNoErr(t, err) + }() + + return volume, err +} + +func createVolumeAttachment(t *testing.T, computeClient *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, serverID string, volumeID string) { + va, err := volumeattach.Create(computeClient, serverID, &osVolumeAttach.CreateOpts{ + VolumeID: volumeID, + }).Extract() + th.AssertNoErr(t, err) + defer func() { + err = osVolumes.WaitForStatus(blockClient, volumeID, "in-use", 60) + th.AssertNoErr(t, err) + err = volumeattach.Delete(computeClient, serverID, va.ID).ExtractErr() + th.AssertNoErr(t, err) + err = osVolumes.WaitForStatus(blockClient, volumeID, "available", 60) + th.AssertNoErr(t, err) + }() + t.Logf("Attached volume to server: %+v", va) +} + +func TestAttachVolume(t *testing.T) { + choices, err := optionsFromEnv() + if err != nil { + t.Fatal(err) + } + + computeClient, err := newClient() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + blockClient, err := newBlockClient(t) + if err != nil { + t.Fatalf("Unable to create a blockstorage client: %v", err) + } + + server, err := createVAServer(t, computeClient, choices) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer func() { + servers.Delete(computeClient, server.ID) + t.Logf("Server deleted.") + }() + + if err = osServers.WaitForStatus(computeClient, server.ID, "ACTIVE", 300); err != nil { + t.Fatalf("Unable to wait for server: %v", err) + } + + volume, err := createVAVolume(t, blockClient) + if err != nil { + t.Fatalf("Unable to create volume: %v", err) + } + defer func() { + err = volumes.Delete(blockClient, volume.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Volume deleted.") + }() + + createVolumeAttachment(t, computeClient, blockClient, server.ID, volume.ID) + +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/extension_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/extension_test.go new file mode 100644 index 000000000000..a50e015522d7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/extension_test.go @@ -0,0 +1,54 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + extensions2 "github.com/rackspace/gophercloud/rackspace/identity/v2/extensions" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestExtensions(t *testing.T) { + service := authenticatedClient(t) + + t.Logf("Extensions available on this identity endpoint:") + count := 0 + var chosen string + err := extensions2.List(service).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + extensions, err := extensions2.ExtractExtensions(page) + th.AssertNoErr(t, err) + + for i, ext := range extensions { + if chosen == "" { + chosen = ext.Alias + } + + t.Logf("[%02d] name=[%s] namespace=[%s]", i, ext.Name, ext.Namespace) + t.Logf(" alias=[%s] updated=[%s]", ext.Alias, ext.Updated) + t.Logf(" description=[%s]", ext.Description) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + + if chosen == "" { + t.Logf("No extensions found.") + return + } + + ext, err := extensions2.Get(service, chosen).Extract() + th.AssertNoErr(t, err) + + t.Logf("Detail for extension [%s]:", chosen) + t.Logf(" name=[%s]", ext.Name) + t.Logf(" namespace=[%s]", ext.Namespace) + t.Logf(" alias=[%s]", ext.Alias) + t.Logf(" updated=[%s]", ext.Updated) + t.Logf(" description=[%s]", ext.Description) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/identity_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/identity_test.go new file mode 100644 index 000000000000..1182982f44de --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/identity_test.go @@ -0,0 +1,50 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions { + // Obtain credentials from the environment. + options, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + options = tools.OnlyRS(options) + + if options.Username == "" { + t.Fatal("Please provide a Rackspace username as RS_USERNAME.") + } + if options.APIKey == "" { + t.Fatal("Please provide a Rackspace API key as RS_API_KEY.") + } + + return options +} + +func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient { + ao := rackspaceAuthOptions(t) + + provider, err := rackspace.NewClient(ao.IdentityEndpoint) + th.AssertNoErr(t, err) + + if auth { + err = rackspace.Authenticate(provider, ao) + th.AssertNoErr(t, err) + } + + return rackspace.NewIdentityV2(provider) +} + +func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient { + return createClient(t, false) +} + +func authenticatedClient(t *testing.T) *gophercloud.ServiceClient { + return createClient(t, true) +} 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 000000000000..5ec3cc8e833b --- /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 000000000000..efaeb75cde8a --- /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/tenant_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tenant_test.go new file mode 100644 index 000000000000..6081a498e34e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/identity/v2/tenant_test.go @@ -0,0 +1,37 @@ +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + rstenants "github.com/rackspace/gophercloud/rackspace/identity/v2/tenants" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestTenants(t *testing.T) { + service := authenticatedClient(t) + + t.Logf("Tenants available to the currently issued token:") + count := 0 + err := rstenants.List(service, nil).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + tenants, err := rstenants.ExtractTenants(page) + th.AssertNoErr(t, err) + + for i, tenant := range tenants { + t.Logf("[%02d] id=[%s]", i, tenant.ID) + t.Logf(" name=[%s] enabled=[%v]", i, tenant.Name, tenant.Enabled) + t.Logf(" description=[%s]", tenant.Description) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No tenants listed for your current token.") + } +} 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 000000000000..28c0c832befb --- /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 000000000000..7a380273c629 --- /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 000000000000..4ce05e69ca84 --- /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 000000000000..c67ddecad933 --- /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 000000000000..c1a8e24dd9bd --- /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 000000000000..18b9fe71ce3b --- /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 000000000000..8d85655f6b1f --- /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 000000000000..1cc12356cafe --- /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 000000000000..bc0c2a89f2a8 --- /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 000000000000..81704187fef5 --- /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 000000000000..3862123bfd9c --- /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 000000000000..3c42bb20cd34 --- /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/security_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/security_test.go new file mode 100644 index 000000000000..ec029913e3b7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/networking/v2/security_test.go @@ -0,0 +1,165 @@ +// +build acceptance networking security + +package v2 + +import ( + "testing" + + osGroups "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups" + osRules "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules" + osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + osPorts "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/pagination" + rsNetworks "github.com/rackspace/gophercloud/rackspace/networking/v2/networks" + rsPorts "github.com/rackspace/gophercloud/rackspace/networking/v2/ports" + rsGroups "github.com/rackspace/gophercloud/rackspace/networking/v2/security/groups" + rsRules "github.com/rackspace/gophercloud/rackspace/networking/v2/security/rules" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestSecurityGroups(t *testing.T) { + Setup(t) + defer Teardown() + + // create security group + groupID := createSecGroup(t) + + // delete security group + defer deleteSecGroup(t, groupID) + + // list security group + listSecGroups(t) + + // get security group + getSecGroup(t, groupID) +} + +func TestSecurityGroupRules(t *testing.T) { + Setup(t) + defer Teardown() + + // create security group + groupID := createSecGroup(t) + + defer deleteSecGroup(t, groupID) + + // create security group rule + ruleID := createSecRule(t, groupID) + + // delete security group rule + defer deleteSecRule(t, ruleID) + + // list security group rule + listSecRules(t) + + // get security group rule + getSecRule(t, ruleID) +} + +func createSecGroup(t *testing.T) string { + sg, err := rsGroups.Create(Client, osGroups.CreateOpts{ + Name: "new-webservers", + Description: "security group for webservers", + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created security group %s", sg.ID) + + return sg.ID +} + +func listSecGroups(t *testing.T) { + err := rsGroups.List(Client, osGroups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + list, err := osGroups.ExtractGroups(page) + if err != nil { + t.Errorf("Failed to extract secgroups: %v", err) + return false, err + } + + for _, sg := range list { + t.Logf("Listing security group: ID [%s] Name [%s]", sg.ID, sg.Name) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getSecGroup(t *testing.T, id string) { + sg, err := rsGroups.Get(Client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting security group: ID [%s] Name [%s] Description [%s]", sg.ID, sg.Name, sg.Description) +} + +func createSecGroupPort(t *testing.T, groupID string) (string, string) { + n, err := rsNetworks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network"}).Extract() + th.AssertNoErr(t, err) + t.Logf("Created network %s", n.ID) + + opts := osPorts.CreateOpts{ + NetworkID: n.ID, + Name: "my_port", + SecurityGroups: []string{groupID}, + } + p, err := rsPorts.Create(Client, opts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created port %s with security group %s", p.ID, groupID) + + return n.ID, p.ID +} + +func deleteSecGroup(t *testing.T, groupID string) { + res := rsGroups.Delete(Client, groupID) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted security group %s", groupID) +} + +func createSecRule(t *testing.T, groupID string) string { + r, err := rsRules.Create(Client, osRules.CreateOpts{ + Direction: "ingress", + PortRangeMin: 80, + EtherType: "IPv4", + PortRangeMax: 80, + Protocol: "tcp", + SecGroupID: groupID, + }).Extract() + + th.AssertNoErr(t, err) + + t.Logf("Created security group rule %s", r.ID) + + return r.ID +} + +func listSecRules(t *testing.T) { + err := rsRules.List(Client, osRules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + list, err := osRules.ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract sec rules: %v", err) + return false, err + } + + for _, r := range list { + t.Logf("Listing security rule: ID [%s]", r.ID) + } + + return true, nil + }) + + th.AssertNoErr(t, err) +} + +func getSecRule(t *testing.T, id string) { + r, err := rsRules.Get(Client, id).Extract() + th.AssertNoErr(t, err) + t.Logf("Getting security rule: ID [%s] Direction [%s] EtherType [%s] Protocol [%s]", + r.ID, r.Direction, r.EtherType, r.Protocol) +} + +func deleteSecRule(t *testing.T, id string) { + res := rsRules.Delete(Client, id) + th.AssertNoErr(t, res.Err) + t.Logf("Deleted security rule %s", id) +} 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 000000000000..c4014320a4c3 --- /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 new file mode 100644 index 000000000000..8b3cde45e459 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/accounts_test.go @@ -0,0 +1,38 @@ +// +build acceptance rackspace + +package v1 + +import ( + "testing" + + raxAccounts "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAccounts(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + 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": ""}}) + th.AssertNoErr(t, updateres.Err) + metadata, err := raxAccounts.Get(c).ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "") + }() + + 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/bulk_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/bulk_test.go new file mode 100644 index 000000000000..79013a564a91 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/bulk_test.go @@ -0,0 +1,23 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestBulk(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + var options bulk.DeleteOpts + options = append(options, "container/object1") + res := bulk.Delete(c, options) + th.AssertNoErr(t, res.Err) + body, err := res.ExtractBody() + th.AssertNoErr(t, err) + t.Logf("Response body from Bulk Delete Request: %+v\n", body) +} 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 new file mode 100644 index 000000000000..0f56f4978af8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go @@ -0,0 +1,66 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "testing" + + osContainers "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + raxCDNContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCDNContainers(t *testing.T) { + raxClient, err := createClient(t, false) + th.AssertNoErr(t, err) + + createres := raxContainers.Create(raxClient, "gophercloud-test", nil) + th.AssertNoErr(t, createres.Err) + t.Logf("Headers from Create Container request: %+v\n", createres.Header) + defer func() { + res := raxContainers.Delete(raxClient, "gophercloud-test") + th.AssertNoErr(t, res.Err) + }() + + raxCDNClient, err := createClient(t, true) + th.AssertNoErr(t, err) + 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 + err = raxCDNContainers.List(raxCDNClient, &osContainers.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + names, err := raxCDNContainers.ExtractNames(page) + th.AssertNoErr(t, err) + + for i, name := range names { + t.Logf("[%02d] %s", i, name) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No CDN containers listed for your current token.") + } + + 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 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/cdnobjects_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go new file mode 100644 index 000000000000..0c0ab8a1ed0e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go @@ -0,0 +1,50 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "bytes" + "testing" + + raxCDNContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers" + raxCDNObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + raxObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCDNObjects(t *testing.T) { + raxClient, err := createClient(t, false) + th.AssertNoErr(t, err) + + createContResult := raxContainers.Create(raxClient, "gophercloud-test", nil) + th.AssertNoErr(t, createContResult.Err) + t.Logf("Headers from Create Container request: %+v\n", createContResult.Header) + defer func() { + deleteResult := raxContainers.Delete(raxClient, "gophercloud-test") + th.AssertNoErr(t, deleteResult.Err) + }() + + header, err := raxObjects.Create(raxClient, "gophercloud-test", "test-object", bytes.NewBufferString("gophercloud cdn test"), nil).ExtractHeader() + th.AssertNoErr(t, err) + t.Logf("Headers from Create Object request: %+v\n", header) + defer func() { + deleteResult := raxObjects.Delete(raxClient, "gophercloud-test", "test-object", nil) + th.AssertNoErr(t, deleteResult.Err) + }() + + raxCDNClient, err := createClient(t, true) + th.AssertNoErr(t, err) + + enableHeader, err := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900}).Extract() + th.AssertNoErr(t, err) + t.Logf("Headers from Enable CDN Container request: %+v\n", enableHeader) + + objCDNURL, err := raxCDNObjects.CDNURL(raxCDNClient, "gophercloud-test", "test-object") + th.AssertNoErr(t, err) + t.Logf("%s CDN URL: %s\n", "test_object", objCDNURL) + + deleteHeader, err := raxCDNObjects.Delete(raxCDNClient, "gophercloud-test", "test-object", nil).Extract() + th.AssertNoErr(t, err) + t.Logf("Headers from Delete CDN Object request: %+v\n", deleteHeader) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/common.go new file mode 100644 index 000000000000..1ae07278cce3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/common.go @@ -0,0 +1,54 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/acceptance/tools" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions { + // Obtain credentials from the environment. + options, err := rackspace.AuthOptionsFromEnv() + th.AssertNoErr(t, err) + options = tools.OnlyRS(options) + + if options.Username == "" { + t.Fatal("Please provide a Rackspace username as RS_USERNAME.") + } + if options.APIKey == "" { + t.Fatal("Please provide a Rackspace API key as RS_API_KEY.") + } + + return options +} + +func createClient(t *testing.T, cdn bool) (*gophercloud.ServiceClient, error) { + region := os.Getenv("RS_REGION") + if region == "" { + t.Fatal("Please provide a Rackspace region as RS_REGION") + } + + ao := rackspaceAuthOptions(t) + + provider, err := rackspace.NewClient(ao.IdentityEndpoint) + th.AssertNoErr(t, err) + + err = rackspace.Authenticate(provider, ao) + th.AssertNoErr(t, err) + + if cdn { + return rackspace.NewObjectCDNV1(provider, gophercloud.EndpointOpts{ + Region: region, + }) + } + + return rackspace.NewObjectStorageV1(provider, gophercloud.EndpointOpts{ + Region: region, + }) +} 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 new file mode 100644 index 000000000000..c89551373f1f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/containers_test.go @@ -0,0 +1,90 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "testing" + + osContainers "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestContainers(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + t.Logf("Containers Info available to the currently issued token:") + count := 0 + err = raxContainers.List(c, &osContainers.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + containers, err := raxContainers.ExtractInfo(page) + th.AssertNoErr(t, err) + + for i, container := range containers { + t.Logf("[%02d] name=[%s]", i, container.Name) + t.Logf(" count=[%d]", container.Count) + t.Logf(" bytes=[%d]", container.Bytes) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No containers listed for your current token.") + } + + t.Logf("Container Names available to the currently issued token:") + count = 0 + err = raxContainers.List(c, &osContainers.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + names, err := raxContainers.ExtractNames(page) + th.AssertNoErr(t, err) + + for i, name := range names { + t.Logf("[%02d] %s", i, name) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No containers listed for your current token.") + } + + 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() { + 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) + }() + + 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 Container request (after update reverted): %+v\n", metadata) + th.CheckEquals(t, metadata["White"], "") + }() + + getres := raxContainers.Get(c, "gophercloud-test") + getHeader, err := getres.Extract() + th.AssertNoErr(t, err) + 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 new file mode 100644 index 000000000000..585dea7696a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/objectstorage/v1/objects_test.go @@ -0,0 +1,124 @@ +// +build acceptance rackspace objectstorage v1 + +package v1 + +import ( + "bytes" + "testing" + + osObjects "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" + raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers" + raxObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestObjects(t *testing.T) { + c, err := createClient(t, false) + th.AssertNoErr(t, err) + + res := raxContainers.Create(c, "gophercloud-test", nil) + th.AssertNoErr(t, res.Err) + + defer func() { + t.Logf("Deleting container...") + res := raxContainers.Delete(c, "gophercloud-test") + th.AssertNoErr(t, res.Err) + }() + + content := bytes.NewBufferString("Lewis Carroll") + 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) + }() + + t.Logf("Objects Info available to the currently issued token:") + count := 0 + err = raxObjects.List(c, "gophercloud-test", &osObjects.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + objects, err := raxObjects.ExtractInfo(page) + th.AssertNoErr(t, err) + + for i, object := range objects { + t.Logf("[%02d] name=[%s]", i, object.Name) + t.Logf(" content-type=[%s]", object.ContentType) + t.Logf(" bytes=[%d]", object.Bytes) + t.Logf(" last-modified=[%s]", object.LastModified) + t.Logf(" hash=[%s]", object.Hash) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No objects listed for your current token.") + } + t.Logf("Container Names available to the currently issued token:") + count = 0 + err = raxObjects.List(c, "gophercloud-test", &osObjects.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page %02d ---", count) + + names, err := raxObjects.ExtractNames(page) + th.AssertNoErr(t, err) + + for i, name := range names { + t.Logf("[%02d] %s", i, name) + } + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + if count == 0 { + t.Errorf("No objects listed for your current token.") + } + + 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) + }() + + o1Content, err := raxObjects.Download(c, "gophercloud-test", "o1", nil).ExtractContent() + th.AssertNoErr(t, err) + o2Content, err := raxObjects.Download(c, "gophercloud-test", "o2", nil).ExtractContent() + th.AssertNoErr(t, err) + th.AssertEquals(t, string(o2Content), string(o1Content)) + + updateres := raxObjects.Update(c, "gophercloud-test", "o2", osObjects.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}) + th.AssertNoErr(t, updateres.Err) + t.Logf("Headers from Update Account request: %+v\n", updateres.Header) + defer func() { + res := raxObjects.Update(c, "gophercloud-test", "o2", osObjects.UpdateOpts{Metadata: map[string]string{"white": ""}}) + th.AssertNoErr(t, res.Err) + 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"]) + }() + + getres := raxObjects.Get(c, "gophercloud-test", "o2", nil) + th.AssertNoErr(t, getres.Err) + t.Logf("Headers from Get Account request (after update): %+v\n", getres.Header) + metadata, err := getres.ExtractMetadata() + th.AssertNoErr(t, err) + t.Logf("Metadata from Get Account request (after update): %+v\n", metadata) + 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/rackspace/orchestration/v1/buildinfo_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/buildinfo_test.go new file mode 100644 index 000000000000..42cc048e3b8b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/buildinfo_test.go @@ -0,0 +1,20 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestBuildInfo(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + bi, err := buildinfo.Get(client).Extract() + th.AssertNoErr(t, err) + t.Logf("retrieved build info: %+v\n", bi) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/common.go new file mode 100644 index 000000000000..b9d51979d7f0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/common.go @@ -0,0 +1,45 @@ +// +build acceptance + +package v1 + +import ( + "fmt" + "os" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace" + th "github.com/rackspace/gophercloud/testhelper" +) + +var template = fmt.Sprintf(` +{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": {}, + "resources": { + "hello_world": { + "type":"OS::Nova::Server", + "properties": { + "flavor": "%s", + "image": "%s", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } +} +`, os.Getenv("RS_FLAVOR_ID"), os.Getenv("RS_IMAGE_ID")) + +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.NewOrchestrationV1(client, gophercloud.EndpointOpts{ + Region: os.Getenv("RS_REGION_NAME"), + }) + th.AssertNoErr(t, err) + return c +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stackevents_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stackevents_test.go new file mode 100644 index 000000000000..9e3fc084ad87 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stackevents_test.go @@ -0,0 +1,70 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + osStackEvents "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents" + osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents" + "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestStackEvents(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName := "postman_stack_2" + resourceName := "hello_world" + var eventID string + + createOpts := osStacks.CreateOpts{ + Name: stackName, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + err = stackevents.List(client, stackName, stack.ID, nil).EachPage(func(page pagination.Page) (bool, error) { + events, err := osStackEvents.ExtractEvents(page) + th.AssertNoErr(t, err) + t.Logf("listed events: %+v\n", events) + eventID = events[0].ID + return false, nil + }) + th.AssertNoErr(t, err) + + err = stackevents.ListResourceEvents(client, stackName, stack.ID, resourceName, nil).EachPage(func(page pagination.Page) (bool, error) { + resourceEvents, err := osStackEvents.ExtractResourceEvents(page) + th.AssertNoErr(t, err) + t.Logf("listed resource events: %+v\n", resourceEvents) + return false, nil + }) + th.AssertNoErr(t, err) + + event, err := stackevents.Get(client, stackName, stack.ID, resourceName, eventID).Extract() + th.AssertNoErr(t, err) + t.Logf("retrieved event: %+v\n", event) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stackresources_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stackresources_test.go new file mode 100644 index 000000000000..65926e78dc67 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stackresources_test.go @@ -0,0 +1,64 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + osStackResources "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources" + osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources" + "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestStackResources(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName := "postman_stack_2" + + createOpts := osStacks.CreateOpts{ + Name: stackName, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + resourceName := "hello_world" + resource, err := stackresources.Get(client, stackName, stack.ID, resourceName).Extract() + th.AssertNoErr(t, err) + t.Logf("Got stack resource: %+v\n", resource) + + metadata, err := stackresources.Metadata(client, stackName, stack.ID, resourceName).Extract() + th.AssertNoErr(t, err) + t.Logf("Got stack resource metadata: %+v\n", metadata) + + err = stackresources.List(client, stackName, stack.ID, nil).EachPage(func(page pagination.Page) (bool, error) { + resources, err := osStackResources.ExtractResources(page) + th.AssertNoErr(t, err) + t.Logf("resources: %+v\n", resources) + return false, nil + }) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stacks_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stacks_test.go new file mode 100644 index 000000000000..cfec4e9817e6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stacks_test.go @@ -0,0 +1,82 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestStacks(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName1 := "gophercloud-test-stack-2" + createOpts := osStacks.CreateOpts{ + Name: stackName1, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName1, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName1) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + updateOpts := osStacks.UpdateOpts{ + Template: template, + Timeout: 20, + } + err = stacks.Update(client, stackName1, stack.ID, updateOpts).ExtractErr() + th.AssertNoErr(t, err) + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "UPDATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + t.Logf("Updated stack") + + err = stacks.List(client, nil).EachPage(func(page pagination.Page) (bool, error) { + stackList, err := osStacks.ExtractStacks(page) + th.AssertNoErr(t, err) + + t.Logf("Got stack list: %+v\n", stackList) + + return true, nil + }) + th.AssertNoErr(t, err) + + getStack, err := stacks.Get(client, stackName1, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Got stack: %+v\n", getStack) + + abandonedStack, err := stacks.Abandon(client, stackName1, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("Abandonded stack %+v\n", abandonedStack) + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stacktemplates_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stacktemplates_test.go new file mode 100644 index 000000000000..1f7b21710654 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/orchestration/v1/stacktemplates_test.go @@ -0,0 +1,79 @@ +// +build acceptance + +package v1 + +import ( + "testing" + + "github.com/rackspace/gophercloud" + osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" + osStacktemplates "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates" + "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks" + "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestStackTemplates(t *testing.T) { + // Create a provider client for making the HTTP requests. + // See common.go in this directory for more information. + client := newClient(t) + + stackName := "postman_stack_2" + + createOpts := osStacks.CreateOpts{ + Name: stackName, + Template: template, + Timeout: 5, + } + stack, err := stacks.Create(client, createOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("Created stack: %+v\n", stack) + defer func() { + err := stacks.Delete(client, stackName, stack.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Deleted stack (%s)", stackName) + }() + err = gophercloud.WaitFor(60, func() (bool, error) { + getStack, err := stacks.Get(client, stackName, stack.ID).Extract() + if err != nil { + return false, err + } + if getStack.Status == "CREATE_COMPLETE" { + return true, nil + } + return false, nil + }) + + tmpl, err := stacktemplates.Get(client, stackName, stack.ID).Extract() + th.AssertNoErr(t, err) + t.Logf("retrieved template: %+v\n", tmpl) + + validateOpts := osStacktemplates.ValidateOpts{ + Template: map[string]interface{}{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": map[string]interface{}{ + "flavor": map[string]interface{}{ + "default": "m1.tiny", + "type": "string", + }, + }, + "resources": map[string]interface{}{ + "hello_world": map[string]interface{}{ + "type": "OS::Nova::Server", + "properties": map[string]interface{}{ + "key_name": "heat_key", + "flavor": map[string]interface{}{ + "get_param": "flavor", + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", + }, + }, + }, + }, + } + validatedTemplate, err := stacktemplates.Validate(client, validateOpts).Extract() + th.AssertNoErr(t, err) + t.Logf("validated template: %+v\n", validatedTemplate) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/pkg.go new file mode 100644 index 000000000000..5d17b32caaa9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/pkg.go @@ -0,0 +1 @@ +package rackspace diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/cloudnetworks_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/cloudnetworks_test.go new file mode 100644 index 000000000000..2c6287e9f773 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/cloudnetworks_test.go @@ -0,0 +1,36 @@ +// +build acceptance + +package v3 + +import ( + "fmt" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCloudNetworks(t *testing.T) { + c := newClient(t) + cnID := testListNetworks(t, c) + testGetNetworks(t, c, cnID) +} + +func testListNetworks(t *testing.T, c *gophercloud.ServiceClient) string { + allPages, err := cloudnetworks.List(c).AllPages() + th.AssertNoErr(t, err) + allcn, err := cloudnetworks.ExtractCloudNetworks(allPages) + fmt.Printf("Listing all cloud networks: %+v\n\n", allcn) + var cnID string + if len(allcn) > 0 { + cnID = allcn[0].ID + } + return cnID +} + +func testGetNetworks(t *testing.T, c *gophercloud.ServiceClient, id string) { + cn, err := cloudnetworks.Get(c, id).Extract() + th.AssertNoErr(t, err) + fmt.Printf("Retrieved cloud network: %+v\n\n", cn) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/common.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/common.go new file mode 100644 index 000000000000..8c7531417450 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/common.go @@ -0,0 +1,26 @@ +// +build acceptance + +package v3 + +import ( + "os" + "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.NewRackConnectV3(client, gophercloud.EndpointOpts{ + Region: os.Getenv("RS_REGION_NAME"), + }) + th.AssertNoErr(t, err) + return c +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/lbpools_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/lbpools_test.go new file mode 100644 index 000000000000..85ac931b9ca6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/lbpools_test.go @@ -0,0 +1,71 @@ +// +build acceptance + +package v3 + +import ( + "fmt" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestLBPools(t *testing.T) { + c := newClient(t) + pID := testListPools(t, c) + testGetPools(t, c, pID) + nID := testListNodes(t, c, pID) + testListNodeDetails(t, c, pID) + testGetNode(t, c, pID, nID) + testGetNodeDetails(t, c, pID, nID) +} + +func testListPools(t *testing.T, c *gophercloud.ServiceClient) string { + allPages, err := lbpools.List(c).AllPages() + th.AssertNoErr(t, err) + allp, err := lbpools.ExtractPools(allPages) + fmt.Printf("Listing all LB pools: %+v\n\n", allp) + var pID string + if len(allp) > 0 { + pID = allp[0].ID + } + return pID +} + +func testGetPools(t *testing.T, c *gophercloud.ServiceClient, pID string) { + p, err := lbpools.Get(c, pID).Extract() + th.AssertNoErr(t, err) + fmt.Printf("Retrieved LB pool: %+v\n\n", p) +} + +func testListNodes(t *testing.T, c *gophercloud.ServiceClient, pID string) string { + allPages, err := lbpools.ListNodes(c, pID).AllPages() + th.AssertNoErr(t, err) + alln, err := lbpools.ExtractNodes(allPages) + fmt.Printf("Listing all LB pool nodes for pool (%s): %+v\n\n", pID, alln) + var nID string + if len(alln) > 0 { + nID = alln[0].ID + } + return nID +} + +func testListNodeDetails(t *testing.T, c *gophercloud.ServiceClient, pID string) { + allPages, err := lbpools.ListNodesDetails(c, pID).AllPages() + th.AssertNoErr(t, err) + alln, err := lbpools.ExtractNodesDetails(allPages) + fmt.Printf("Listing all LB pool nodes details for pool (%s): %+v\n\n", pID, alln) +} + +func testGetNode(t *testing.T, c *gophercloud.ServiceClient, pID, nID string) { + n, err := lbpools.GetNode(c, pID, nID).Extract() + th.AssertNoErr(t, err) + fmt.Printf("Retrieved LB node: %+v\n\n", n) +} + +func testGetNodeDetails(t *testing.T, c *gophercloud.ServiceClient, pID, nID string) { + n, err := lbpools.GetNodeDetails(c, pID, nID).Extract() + th.AssertNoErr(t, err) + fmt.Printf("Retrieved LB node details: %+v\n\n", n) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/publicips_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/publicips_test.go new file mode 100644 index 000000000000..8dc62703ba70 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/rackspace/rackconnect/v3/publicips_test.go @@ -0,0 +1,45 @@ +// +build acceptance + +package v3 + +import ( + "fmt" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestPublicIPs(t *testing.T) { + c := newClient(t) + ipID := testListIPs(t, c) + sID := testGetIP(t, c, ipID) + testListIPsForServer(t, c, sID) +} + +func testListIPs(t *testing.T, c *gophercloud.ServiceClient) string { + allPages, err := publicips.List(c).AllPages() + th.AssertNoErr(t, err) + allip, err := publicips.ExtractPublicIPs(allPages) + fmt.Printf("Listing all public IPs: %+v\n\n", allip) + var ipID string + if len(allip) > 0 { + ipID = allip[0].ID + } + return ipID +} + +func testGetIP(t *testing.T, c *gophercloud.ServiceClient, ipID string) string { + ip, err := publicips.Get(c, ipID).Extract() + th.AssertNoErr(t, err) + fmt.Printf("Retrieved public IP (%s): %+v\n\n", ipID, ip) + return ip.CloudServer.ID +} + +func testListIPsForServer(t *testing.T, c *gophercloud.ServiceClient, sID string) { + allPages, err := publicips.ListForServer(c, sID).AllPages() + th.AssertNoErr(t, err) + allip, err := publicips.ExtractPublicIPs(allPages) + fmt.Printf("Listing all public IPs for server (%s): %+v\n\n", sID, allip) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/pkg.go new file mode 100644 index 000000000000..f7eca1298a7f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/pkg.go @@ -0,0 +1 @@ +package tools 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 new file mode 100644 index 000000000000..35679b728c3e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/tools/tools.go @@ -0,0 +1,89 @@ +// +build acceptance common + +package tools + +import ( + "crypto/rand" + "errors" + mrand "math/rand" + "os" + "time" + + "github.com/rackspace/gophercloud" +) + +// ErrTimeout is returned if WaitFor takes longer than 300 second to happen. +var ErrTimeout = errors.New("Timed out") + +// OnlyRS overrides the default Gophercloud behavior of using OS_-prefixed environment variables +// if RS_ variables aren't present. Otherwise, they'll stomp over each other here in the acceptance +// tests, where you need to have both defined. +func OnlyRS(original gophercloud.AuthOptions) gophercloud.AuthOptions { + if os.Getenv("RS_AUTH_URL") == "" { + original.IdentityEndpoint = "" + } + if os.Getenv("RS_USERNAME") == "" { + original.Username = "" + } + if os.Getenv("RS_PASSWORD") == "" { + original.Password = "" + } + if os.Getenv("RS_API_KEY") == "" { + original.APIKey = "" + } + return original +} + +// WaitFor polls a predicate function once per second to wait for a certain state to arrive. +func WaitFor(predicate func() (bool, error)) error { + for i := 0; i < 300; i++ { + time.Sleep(1 * time.Second) + + satisfied, err := predicate() + if err != nil { + return err + } + if satisfied { + return nil + } + } + return ErrTimeout +} + +// MakeNewPassword generates a new string that's guaranteed to be different than the given one. +func MakeNewPassword(oldPass string) string { + randomPassword := RandomString("", 16) + for randomPassword == oldPass { + randomPassword = RandomString("", 16) + } + return randomPassword +} + +// RandomString generates a string of given length, but random content. +// All content will be within the ASCII graphic character set. +// (Implementation from Even Shaw's contribution on +// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go). +func RandomString(prefix string, n int) string { + const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + var bytes = make([]byte, n) + rand.Read(bytes) + for i, b := range bytes { + bytes[i] = alphanum[b%byte(len(alphanum))] + } + 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 { + if len(value) > 40 { + return value[0:37] + "..." + } + return value +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go new file mode 100644 index 000000000000..9819e45f2d75 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_options.go @@ -0,0 +1,46 @@ +package gophercloud + +/* +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. 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 are needed. + Username, UserID string + + // 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 + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID, DomainName string + + // 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 + // how these fields influence authentication. + TenantID, TenantName string + + // AllowReauth should be set to true if you grant permission for Gophercloud to + // cache your credentials in memory, and to allow Gophercloud to attempt to + // 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. + 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 new file mode 100644 index 000000000000..856a23382e90 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/auth_results.go @@ -0,0 +1,14 @@ +package gophercloud + +import "time" + +// 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) + + // ExpiresAt retrieves the token's expiration time. + ExpiresAt() (time.Time, 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 000000000000..fb81a9d8f174 --- /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 new file mode 100644 index 000000000000..5189431212c1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search.go @@ -0,0 +1,92 @@ +package gophercloud + +import "errors" + +var ( + // 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. 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 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 indicates that an endpoint is only available to + // administrators. + AvailabilityAdmin Availability = "admin" + + // AvailabilityPublic indicates that an endpoint is available to everyone on + // the internet. + AvailabilityPublic Availability = "public" + + // AvailabilityInternal indicates that an endpoint is only available within + // the cluster's internal network. + AvailabilityInternal Availability = "internal" +) + +// 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 [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 [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 [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 [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 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 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 + } + if eo.Availability == "" { + eo.Availability = AvailabilityPublic + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search_test.go new file mode 100644 index 000000000000..34574534274c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/endpoint_search_test.go @@ -0,0 +1,19 @@ +package gophercloud + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestApplyDefaultsToEndpointOpts(t *testing.T) { + eo := EndpointOpts{Availability: AvailabilityPublic} + eo.ApplyDefaults("compute") + expected := EndpointOpts{Availability: AvailabilityPublic, Type: "compute"} + th.CheckDeepEquals(t, expected, eo) + + eo = EndpointOpts{Type: "compute"} + eo.ApplyDefaults("object-store") + expected = EndpointOpts{Availability: AvailabilityPublic, Type: "compute"} + th.CheckDeepEquals(t, expected, eo) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/auth_env.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/auth_env.go new file mode 100644 index 000000000000..a4402b6f06d9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/auth_env.go @@ -0,0 +1,58 @@ +package openstack + +import ( + "fmt" + "os" + + "github.com/rackspace/gophercloud" +) + +var nilOptions = gophercloud.AuthOptions{} + +// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the required OS_AUTH_URL, OS_USERNAME, or OS_PASSWORD +// environment variables, respectively, remain undefined. See the AuthOptions() function for more details. +var ( + ErrNoAuthURL = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.") + ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.") + ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD needs to be set.") +) + +// AuthOptions fills out an identity.AuthOptions structure with the settings found on the various OpenStack +// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, +// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must +// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional. +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := os.Getenv("OS_AUTH_URL") + username := os.Getenv("OS_USERNAME") + userID := os.Getenv("OS_USERID") + password := os.Getenv("OS_PASSWORD") + tenantID := os.Getenv("OS_TENANT_ID") + tenantName := os.Getenv("OS_TENANT_NAME") + domainID := os.Getenv("OS_DOMAIN_ID") + domainName := os.Getenv("OS_DOMAIN_NAME") + + if authURL == "" { + return nilOptions, ErrNoAuthURL + } + + if username == "" && userID == "" { + return nilOptions, ErrNoUsername + } + + if password == "" { + return nilOptions, ErrNoPassword + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + UserID: userID, + Username: username, + Password: password, + TenantID: tenantID, + TenantName: tenantName, + DomainID: domainID, + DomainName: domainName, + } + + return ao, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/doc.go new file mode 100644 index 000000000000..e3af39f513a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/doc.go @@ -0,0 +1,3 @@ +// Package apiversions provides information and interaction with the different +// API versions for the OpenStack Block Storage service, code-named Cinder. +package apiversions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests.go new file mode 100644 index 000000000000..bb2c25915849 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests.go @@ -0,0 +1,21 @@ +package apiversions + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List lists all the Cinder API versions available to end-users. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} + +// Get will retrieve the volume type with the provided ID. To extract the volume +// type from the result, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, v string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, v), &res.Body, nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests_test.go new file mode 100644 index 000000000000..56b5e4fc72bd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/requests_test.go @@ -0,0 +1,145 @@ +package apiversions + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "versions": [ + { + "status": "CURRENT", + "updated": "2012-01-04T11:33:21Z", + "id": "v1.0", + "links": [ + { + "href": "http://23.253.228.211:8776/v1/", + "rel": "self" + } + ] + }, + { + "status": "CURRENT", + "updated": "2012-11-21T11:33:21Z", + "id": "v2.0", + "links": [ + { + "href": "http://23.253.228.211:8776/v2/", + "rel": "self" + } + ] + } + ] + }`) + }) + + count := 0 + + List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractAPIVersions(page) + if err != nil { + t.Errorf("Failed to extract API versions: %v", err) + return false, err + } + + expected := []APIVersion{ + APIVersion{ + ID: "v1.0", + Status: "CURRENT", + Updated: "2012-01-04T11:33:21Z", + }, + APIVersion{ + ID: "v2.0", + Status: "CURRENT", + Updated: "2012-11-21T11:33:21Z", + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestAPIInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "version": { + "status": "CURRENT", + "updated": "2012-01-04T11:33:21Z", + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.volume+xml;version=1" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.volume+json;version=1" + } + ], + "id": "v1.0", + "links": [ + { + "href": "http://23.253.228.211:8776/v1/", + "rel": "self" + }, + { + "href": "http://jorgew.github.com/block-storage-api/content/os-block-storage-1.0.pdf", + "type": "application/pdf", + "rel": "describedby" + }, + { + "href": "http://docs.rackspacecloud.com/servers/api/v1.1/application.wadl", + "type": "application/vnd.sun.wadl+xml", + "rel": "describedby" + } + ] + } + }`) + }) + + actual, err := Get(client.ServiceClient(), "v1").Extract() + if err != nil { + t.Errorf("Failed to extract version: %v", err) + } + + expected := APIVersion{ + ID: "v1.0", + Status: "CURRENT", + Updated: "2012-01-04T11:33:21Z", + } + + th.AssertEquals(t, actual.ID, expected.ID) + th.AssertEquals(t, actual.Status, expected.Status) + th.AssertEquals(t, actual.Updated, expected.Updated) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/results.go new file mode 100644 index 000000000000..7b0df115b507 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/results.go @@ -0,0 +1,58 @@ +package apiversions + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// APIVersion represents an API version for Cinder. +type APIVersion struct { + ID string `json:"id" mapstructure:"id"` // unique identifier + Status string `json:"status" mapstructure:"status"` // current status + Updated string `json:"updated" mapstructure:"updated"` // date last updated +} + +// APIVersionPage is the page returned by a pager when traversing over a +// collection of API versions. +type APIVersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an APIVersionPage struct is empty. +func (r APIVersionPage) IsEmpty() (bool, error) { + is, err := ExtractAPIVersions(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractAPIVersions takes a collection page, extracts all of the elements, +// and returns them a slice of APIVersion structs. It is effectively a cast. +func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) { + var resp struct { + Versions []APIVersion `mapstructure:"versions"` + } + + err := mapstructure.Decode(page.(APIVersionPage).Body, &resp) + + return resp.Versions, err +} + +// GetResult represents the result of a get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts an API version resource. +func (r GetResult) Extract() (*APIVersion, error) { + var resp struct { + Version *APIVersion `mapstructure:"version"` + } + + err := mapstructure.Decode(r.Body, &resp) + + return resp.Version, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls.go new file mode 100644 index 000000000000..56f8260a25c2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls.go @@ -0,0 +1,15 @@ +package apiversions + +import ( + "strings" + + "github.com/rackspace/gophercloud" +) + +func getURL(c *gophercloud.ServiceClient, version string) string { + return c.ServiceURL(strings.TrimRight(version, "/") + "/") +} + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls_test.go new file mode 100644 index 000000000000..37e91425b5ae --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/apiversions/urls_test.go @@ -0,0 +1,26 @@ +package apiversions + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "v1") + expected := endpoint + "v1/" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/doc.go new file mode 100644 index 000000000000..198f83077c59 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/doc.go @@ -0,0 +1,5 @@ +// Package snapshots provides information and interaction with snapshots in the +// OpenStack Block Storage service. A snapshot is a point in time copy of the +// data contained in an external storage volume, and can be controlled +// programmatically. +package snapshots diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/fixtures.go new file mode 100644 index 000000000000..d1461fb69d2b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/fixtures.go @@ -0,0 +1,114 @@ +package snapshots + +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("/snapshots", 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, ` + { + "snapshots": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "display_name": "snapshot-001" + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "display_name": "snapshot-002" + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/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, ` +{ + "snapshot": { + "display_name": "snapshot-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots", 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, ` +{ + "snapshot": { + "volume_id": "1234", + "display_name": "snapshot-001" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "snapshot": { + "volume_id": "1234", + "display_name": "snapshot-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockUpdateMetadataResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/123/metadata", 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.TestJSONRequest(t, r, ` + { + "metadata": { + "key": "v1" + } + } + `) + + fmt.Fprintf(w, ` + { + "metadata": { + "key": "v1" + } + } + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", 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/blockstorage/v1/snapshots/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go new file mode 100644 index 000000000000..d2f10aa6b592 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests.go @@ -0,0 +1,173 @@ +package snapshots + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSnapshotCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Snapshot. This object is passed to +// the snapshots.Create function. For more information about these parameters, +// see the Snapshot object. +type CreateOpts struct { + // OPTIONAL + Description string + // OPTIONAL + Force bool + // OPTIONAL + Metadata map[string]interface{} + // OPTIONAL + Name string + // REQUIRED + VolumeID string +} + +// ToSnapshotCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.VolumeID == "" { + return nil, fmt.Errorf("Required CreateOpts field 'VolumeID' not set.") + } + s["volume_id"] = opts.VolumeID + + if opts.Description != "" { + s["display_description"] = opts.Description + } + if opts.Force == true { + s["force"] = opts.Force + } + if opts.Metadata != nil { + s["metadata"] = opts.Metadata + } + if opts.Name != "" { + s["display_name"] = opts.Name + } + + return map[string]interface{}{"snapshot": s}, nil +} + +// Create will create a new Snapshot based on the values in CreateOpts. To +// extract the Snapshot object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToSnapshotCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return res +} + +// Delete will delete the existing Snapshot with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(deleteURL(client, id), nil) + return res +} + +// Get retrieves the Snapshot with the provided ID. To extract the Snapshot +// object from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToSnapshotListQuery() (string, error) +} + +// ListOpts hold options for listing Snapshots. It is passed to the +// snapshots.List function. +type ListOpts struct { + Name string `q:"display_name"` + Status string `q:"status"` + VolumeID string `q:"volume_id"` +} + +// ToSnapshotListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSnapshotListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns Snapshots optionally limited by the conditions provided in +// ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToSnapshotListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPage := func(r pagination.PageResult) pagination.Page { + return ListResult{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, url, createPage) +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateMetadataOptsBuilder interface { + ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) +} + +// UpdateMetadataOpts contain options for updating an existing Snapshot. This +// object is passed to the snapshots.Update function. For more information +// about the parameters, see the Snapshot object. +type UpdateMetadataOpts struct { + Metadata map[string]interface{} +} + +// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of +// an UpdateMetadataOpts. +func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Metadata != nil { + v["metadata"] = opts.Metadata + } + + return v, nil +} + +// UpdateMetadata will update the Snapshot with provided information. To +// extract the updated Snapshot from the response, call the ExtractMetadata +// method on the UpdateMetadataResult. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult { + var res UpdateMetadataResult + + reqBody, err := opts.ToSnapshotUpdateMetadataMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Put(updateMetadataURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests_test.go new file mode 100644 index 000000000000..d0f9e887e81d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/requests_test.go @@ -0,0 +1,104 @@ +package snapshots + +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() + + MockListResponse(t) + + count := 0 + + List(client.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractSnapshots(page) + if err != nil { + t.Errorf("Failed to extract snapshots: %v", err) + return false, err + } + + expected := []Snapshot{ + Snapshot{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "snapshot-001", + }, + Snapshot{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "snapshot-002", + }, + } + + 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() + + MockGetResponse(t) + + v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "snapshot-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := CreateOpts{VolumeID: "1234", Name: "snapshot-001"} + n, err := Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.VolumeID, "1234") + th.AssertEquals(t, n.Name, "snapshot-001") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestUpdateMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateMetadataResponse(t) + + expected := map[string]interface{}{"key": "v1"} + + options := &UpdateMetadataOpts{ + Metadata: map[string]interface{}{ + "key": "v1", + }, + } + + actual, err := UpdateMetadata(client.ServiceClient(), "123", options).ExtractMetadata() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/results.go new file mode 100644 index 000000000000..e595798e4ae6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/results.go @@ -0,0 +1,123 @@ +package snapshots + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Snapshot contains all the information associated with an OpenStack Snapshot. +type Snapshot struct { + // Currect status of the Snapshot. + Status string `mapstructure:"status"` + + // Display name. + Name string `mapstructure:"display_name"` + + // Instances onto which the Snapshot is attached. + Attachments []string `mapstructure:"attachments"` + + // Logical group. + AvailabilityZone string `mapstructure:"availability_zone"` + + // Is the Snapshot bootable? + Bootable string `mapstructure:"bootable"` + + // Date created. + CreatedAt string `mapstructure:"created_at"` + + // Display description. + Description string `mapstructure:"display_discription"` + + // See VolumeType object for more information. + VolumeType string `mapstructure:"volume_type"` + + // ID of the Snapshot from which this Snapshot was created. + SnapshotID string `mapstructure:"snapshot_id"` + + // ID of the Volume from which this Snapshot was created. + VolumeID string `mapstructure:"volume_id"` + + // User-defined key-value pairs. + Metadata map[string]string `mapstructure:"metadata"` + + // Unique identifier. + ID string `mapstructure:"id"` + + // Size of the Snapshot, in GB. + Size int `mapstructure:"size"` +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ListResult is a pagination.Pager that is returned from a call to the List function. +type ListResult struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Snapshots. +func (r ListResult) IsEmpty() (bool, error) { + volumes, err := ExtractSnapshots(r) + if err != nil { + return true, err + } + return len(volumes) == 0, nil +} + +// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call. +func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) { + var response struct { + Snapshots []Snapshot `json:"snapshots"` + } + + err := mapstructure.Decode(page.(ListResult).Body, &response) + return response.Snapshots, err +} + +// UpdateMetadataResult contains the response body and error from an UpdateMetadata request. +type UpdateMetadataResult struct { + commonResult +} + +// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata. +func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) { + if r.Err != nil { + return nil, r.Err + } + + m := r.Body.(map[string]interface{})["metadata"] + return m.(map[string]interface{}), nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Snapshot object out of the commonResult object. +func (r commonResult) Extract() (*Snapshot, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Snapshot *Snapshot `json:"snapshot"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Snapshot, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls.go new file mode 100644 index 000000000000..4d635e8dd454 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls.go @@ -0,0 +1,27 @@ +package snapshots + +import "github.com/rackspace/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("snapshots") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func metadataURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id, "metadata") +} + +func updateMetadataURL(c *gophercloud.ServiceClient, id string) string { + return metadataURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls_test.go new file mode 100644 index 000000000000..feacf7f69b5c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/urls_test.go @@ -0,0 +1,50 @@ +package snapshots + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "snapshots" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo" + th.AssertEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "snapshots" + th.AssertEquals(t, expected, actual) +} + +func TestMetadataURL(t *testing.T) { + actual := metadataURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo/metadata" + th.AssertEquals(t, expected, actual) +} + +func TestUpdateMetadataURL(t *testing.T) { + actual := updateMetadataURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo/metadata" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util.go new file mode 100644 index 000000000000..64cdc607ecda --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots/util.go @@ -0,0 +1,22 @@ +package snapshots + +import ( + "github.com/rackspace/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/doc.go new file mode 100644 index 000000000000..307b8b12d2f0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/doc.go @@ -0,0 +1,5 @@ +// Package volumes provides information and interaction with volumes in the +// OpenStack Block Storage service. A volume is a detachable block storage +// device, akin to a USB hard drive. It can only be attached to one instance at +// a time. +package volumes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go new file mode 100644 index 000000000000..a1b8697c2a73 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/fixtures.go @@ -0,0 +1,113 @@ +package volumes + +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("/volumes", 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, ` + { + "volumes": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "display_name": "vol-001" + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "display_name": "vol-002" + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/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, ` +{ + "volume": { + "display_name": "vol-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "attachments": [ + { + "device": "/dev/vde", + "server_id": "a740d24b-dc5b-4d59-ac75-53971c2920ba", + "id": "d6da11e5-2ed3-413e-88d8-b772ba62193d", + "volume_id": "d6da11e5-2ed3-413e-88d8-b772ba62193d" + } + ] + } +} + `) + }) +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes", 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, ` +{ + "volume": { + "size": 75 + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "volume": { + "size": 4, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", 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 MockUpdateResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "volume": { + "display_name": "vol-002", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } + } + `) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go new file mode 100644 index 000000000000..253aaf7c5450 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests.go @@ -0,0 +1,203 @@ +package volumes + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Volume. This object is passed to +// the volumes.Create function. For more information about these parameters, +// see the Volume object. +type CreateOpts struct { + // OPTIONAL + Availability string + // OPTIONAL + Description string + // OPTIONAL + Metadata map[string]string + // OPTIONAL + Name string + // REQUIRED + Size int + // OPTIONAL + SnapshotID, SourceVolID, ImageID string + // OPTIONAL + VolumeType string +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Size == 0 { + return nil, fmt.Errorf("Required CreateOpts field 'Size' not set.") + } + v["size"] = opts.Size + + if opts.Availability != "" { + v["availability_zone"] = opts.Availability + } + if opts.Description != "" { + v["display_description"] = opts.Description + } + if opts.ImageID != "" { + v["imageRef"] = opts.ImageID + } + if opts.Metadata != nil { + v["metadata"] = opts.Metadata + } + if opts.Name != "" { + v["display_name"] = opts.Name + } + if opts.SourceVolID != "" { + v["source_volid"] = opts.SourceVolID + } + if opts.SnapshotID != "" { + v["snapshot_id"] = opts.SnapshotID + } + if opts.VolumeType != "" { + v["volume_type"] = opts.VolumeType + } + + return map[string]interface{}{"volume": v}, nil +} + +// Create will create a new Volume based on the values in CreateOpts. To extract +// the Volume object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToVolumeCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return res +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(deleteURL(client, id), nil) + return res +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVolumeListQuery() (string, error) +} + +// ListOpts holds options for listing Volumes. It is passed to the volumes.List +// function. +type ListOpts struct { + // admin-only option. Set it to true to see all tenant volumes. + AllTenants bool `q:"all_tenants"` + // List only volumes that contain Metadata. + Metadata map[string]string `q:"metadata"` + // List only volumes that have Name as the display name. + Name string `q:"name"` + // List only volumes that have a status of Status. + Status string `q:"status"` +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns Volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVolumeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + createPage := func(r pagination.PageResult) pagination.Page { + return ListResult{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, url, createPage) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVolumeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + // OPTIONAL + Name string + // OPTIONAL + Description string + // OPTIONAL + Metadata map[string]string +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Description != "" { + v["display_description"] = opts.Description + } + if opts.Metadata != nil { + v["metadata"] = opts.Metadata + } + if opts.Name != "" { + v["display_name"] = opts.Name + } + + return map[string]interface{}{"volume": v}, nil +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToVolumeUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Put(updateURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go new file mode 100644 index 000000000000..c484cf08df3a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/requests_test.go @@ -0,0 +1,122 @@ +package volumes + +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() + + MockListResponse(t) + + count := 0 + + List(client.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVolumes(page) + if err != nil { + t.Errorf("Failed to extract volumes: %v", err) + return false, err + } + + expected := []Volume{ + Volume{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-001", + }, + Volume{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-002", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestListAll(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListResponse(t) + + allPages, err := List(client.ServiceClient(), &ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := ExtractVolumes(allPages) + th.AssertNoErr(t, err) + + expected := []Volume{ + Volume{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-001", + }, + Volume{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-002", + }, + } + + th.CheckDeepEquals(t, expected, actual) + +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "vol-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertEquals(t, v.Attachments[0]["device"], "/dev/vde") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := &CreateOpts{Size: 75} + n, err := Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Size, 4) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateResponse(t) + + options := UpdateOpts{Name: "vol-002"} + v, err := Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, "vol-002", v.Name) +} 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 new file mode 100644 index 000000000000..2fd4ef14eae0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/results.go @@ -0,0 +1,113 @@ +package volumes + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Volume contains all the information associated with an OpenStack Volume. +type Volume struct { + // Current status of the volume. + Status string `mapstructure:"status"` + + // Human-readable display name for the volume. + Name string `mapstructure:"display_name"` + + // Instances onto which the volume is attached. + Attachments []map[string]interface{} `mapstructure:"attachments"` + + // This parameter is no longer used. + AvailabilityZone string `mapstructure:"availability_zone"` + + // Indicates whether this is a bootable volume. + Bootable string `mapstructure:"bootable"` + + // The date when this volume was created. + CreatedAt string `mapstructure:"created_at"` + + // Human-readable description for the volume. + Description string `mapstructure:"display_description"` + + // The type of volume to create, either SATA or SSD. + VolumeType string `mapstructure:"volume_type"` + + // The ID of the snapshot from which the volume was created + SnapshotID string `mapstructure:"snapshot_id"` + + // The ID of another block storage volume from which the current volume was created + SourceVolID string `mapstructure:"source_volid"` + + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `mapstructure:"metadata"` + + // Unique identifier for the volume. + ID string `mapstructure:"id"` + + // Size of the volume in GB. + Size int `mapstructure:"size"` +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ListResult is a pagination.pager that is returned from a call to the List function. +type ListResult struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r ListResult) IsEmpty() (bool, error) { + volumes, err := ExtractVolumes(r) + if err != nil { + return true, err + } + return len(volumes) == 0, nil +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(page pagination.Page) ([]Volume, error) { + var response struct { + Volumes []Volume `json:"volumes"` + } + + err := mapstructure.Decode(page.(ListResult).Body, &response) + return response.Volumes, err +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Volume *Volume `json:"volume"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Volume, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls.go new file mode 100644 index 000000000000..29629a1af8d6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/rackspace/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls_test.go new file mode 100644 index 000000000000..a95270e14cb4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/urls_test.go @@ -0,0 +1,44 @@ +package volumes + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "volumes" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "volumes" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "volumes/foo" + th.AssertEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "volumes/foo" + th.AssertEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "volumes/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util.go new file mode 100644 index 000000000000..1dda695ea091 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/rackspace/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go new file mode 100644 index 000000000000..793084f89b20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/doc.go @@ -0,0 +1,9 @@ +// Package volumetypes provides information and interaction with volume types +// in the OpenStack Block Storage service. A volume type indicates the type of +// a block storage volume, such as SATA, SCSCI, SSD, etc. These can be +// customized or defined by the OpenStack admin. +// +// You can also define extra_specs associated with your volume types. For +// instance, you could have a VolumeType=SATA, with extra_specs (RPM=10000, +// RAID-Level=5) . Extra_specs are defined and customized by the admin. +package volumetypes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/fixtures.go new file mode 100644 index 000000000000..e3326eae1461 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/fixtures.go @@ -0,0 +1,60 @@ +package volumetypes + +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("/types", 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, ` + { + "volume_types": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "name": "vol-type-001", + "extra_specs": { + "capabilities": "gpu" + } + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "name": "vol-type-002", + "extra_specs": {} + } + ] + } + `) + }) +} + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc("/types/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, ` +{ + "volume_type": { + "name": "vol-type-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "extra_specs": { + "serverNumber": "2" + } + } +} + `) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go new file mode 100644 index 000000000000..1673d13aaf33 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests.go @@ -0,0 +1,76 @@ +package volumetypes + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeTypeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts are options for creating a volume type. +type CreateOpts struct { + // OPTIONAL. See VolumeType. + ExtraSpecs map[string]interface{} + // OPTIONAL. See VolumeType. + Name string +} + +// ToVolumeTypeCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToVolumeTypeCreateMap() (map[string]interface{}, error) { + vt := make(map[string]interface{}) + + if opts.ExtraSpecs != nil { + vt["extra_specs"] = opts.ExtraSpecs + } + if opts.Name != "" { + vt["name"] = opts.Name + } + + return map[string]interface{}{"volume_type": vt}, nil +} + +// Create will create a new volume. To extract the created volume type object, +// call the Extract method on the CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToVolumeTypeCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return res +} + +// Delete will delete the volume type with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(deleteURL(client, id), nil) + return res +} + +// Get will retrieve the volume type with the provided ID. To extract the volume +// type from the result, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, err := client.Get(getURL(client, id), &res.Body, nil) + res.Err = err + return res +} + +// List returns all volume types. +func List(client *gophercloud.ServiceClient) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return ListResult{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, listURL(client), createPage) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests_test.go new file mode 100644 index 000000000000..8d40bfe1d487 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/requests_test.go @@ -0,0 +1,118 @@ +package volumetypes + +import ( + "fmt" + "net/http" + "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() + + MockListResponse(t) + + count := 0 + + List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVolumeTypes(page) + if err != nil { + t.Errorf("Failed to extract volume types: %v", err) + return false, err + } + + expected := []VolumeType{ + VolumeType{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-type-001", + ExtraSpecs: map[string]interface{}{ + "capabilities": "gpu", + }, + }, + VolumeType{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-type-002", + ExtraSpecs: map[string]interface{}{}, + }, + } + + 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() + + MockGetResponse(t) + + vt, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"}) + th.AssertEquals(t, vt.Name, "vol-type-001") + th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "volume_type": { + "name": "vol-type-001" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "volume_type": { + "name": "vol-type-001", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + } +} + `) + }) + + options := &CreateOpts{Name: "vol-type-001"} + n, err := Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "vol-type-001") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusAccepted) + }) + + err := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/results.go new file mode 100644 index 000000000000..c049a045d8cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/results.go @@ -0,0 +1,72 @@ +package volumetypes + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// VolumeType contains all information associated with an OpenStack Volume Type. +type VolumeType struct { + ExtraSpecs map[string]interface{} `json:"extra_specs" mapstructure:"extra_specs"` // user-defined metadata + ID string `json:"id" mapstructure:"id"` // unique identifier + Name string `json:"name" mapstructure:"name"` // display name +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ListResult is a pagination.Pager that is returned from a call to the List function. +type ListResult struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Volume Types. +func (r ListResult) IsEmpty() (bool, error) { + volumeTypes, err := ExtractVolumeTypes(r) + if err != nil { + return true, err + } + return len(volumeTypes) == 0, nil +} + +// ExtractVolumeTypes extracts and returns Volume Types. +func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) { + var response struct { + VolumeTypes []VolumeType `mapstructure:"volume_types"` + } + + err := mapstructure.Decode(page.(ListResult).Body, &response) + return response.VolumeTypes, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume Type object out of the commonResult object. +func (r commonResult) Extract() (*VolumeType, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.VolumeType, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go new file mode 100644 index 000000000000..cf8367bfab1b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls.go @@ -0,0 +1,19 @@ +package volumetypes + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("types") +} + +func createURL(c *gophercloud.ServiceClient) string { + return listURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("types", id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls_test.go new file mode 100644 index 000000000000..44016e295496 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes/urls_test.go @@ -0,0 +1,38 @@ +package volumetypes + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "types" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "types" + th.AssertEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "types/foo" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "types/foo" + th.AssertEquals(t, expected, actual) +} 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 000000000000..f78d4f73551a --- /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 000000000000..19b5ece46143 --- /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 000000000000..dd221bc983bc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/base/requests.go @@ -0,0 +1,21 @@ +package base + +import "github.com/rackspace/gophercloud" + +// Get retrieves the home document, allowing the user to discover the +// entire API. +func Get(c *gophercloud.ServiceClient) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c), &res.Body, nil) + return res +} + +// Ping retrieves a ping to the server. +func Ping(c *gophercloud.ServiceClient) PingResult { + var res PingResult + _, res.Err = c.Get(pingURL(c), nil, &gophercloud.RequestOpts{ + OkCodes: []int{204}, + MoreHeaders: map[string]string{"Accept": ""}, + }) + 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 000000000000..2c20a71103fa --- /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 000000000000..bef1da8a1757 --- /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 000000000000..a95e18bca935 --- /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 000000000000..d4066985cb25 --- /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 000000000000..d7ec1a00d534 --- /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 000000000000..8755a95b8f91 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/requests.go @@ -0,0 +1,22 @@ +package flavors + +import ( + "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 = c.Get(getURL(c, id), &res.Body, nil) + 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 000000000000..f7317382797b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/flavors/requests_test.go @@ -0,0 +1,89 @@ +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 000000000000..8cab48b5367d --- /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 000000000000..6eb38d2939fa --- /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 000000000000..ceecaa5a5e44 --- /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 000000000000..5c6b5d00e498 --- /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 000000000000..1ddc65fafd37 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets/requests.go @@ -0,0 +1,48 @@ +package serviceassets + +import ( + "strings" + + "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 = c.Delete(url, nil) + 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 000000000000..dde7bc171d2c --- /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 000000000000..1d8734b7c32e --- /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 000000000000..cb0aea8fca58 --- /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 000000000000..41f7c60dae21 --- /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 000000000000..359584c2a666 --- /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 000000000000..d9bc9f20b778 --- /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 000000000000..8b37928e219b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/cdn/v1/services/requests.go @@ -0,0 +1,378 @@ +package services + +import ( + "fmt" + "strings" + + "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 := c.Post(createURL(c), &reqBody, nil, nil) + res.Header = resp.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 = c.Get(url, &res.Body, nil) + 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 := c.Request("PATCH", url, gophercloud.RequestOpts{ + JSONBody: &reqBody, + OkCodes: []int{202}, + }) + var result UpdateResult + result.Header = resp.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 = c.Delete(url, nil) + 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 000000000000..59e826f048db --- /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 000000000000..33406c482b76 --- /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 000000000000..d953d4c19814 --- /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 new file mode 100644 index 000000000000..1193b19a7aff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client.go @@ -0,0 +1,263 @@ +package openstack + +import ( + "fmt" + "net/url" + + "github.com/rackspace/gophercloud" + tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens" + "github.com/rackspace/gophercloud/openstack/utils" +) + +const ( + v20 = "v2.0" + v30 = "v3.0" +) + +// NewClient prepares an unauthenticated ProviderClient instance. +// Most users will probably prefer using the AuthenticatedClient function instead. +// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly, +// for example. +func NewClient(endpoint string) (*gophercloud.ProviderClient, error) { + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + hadPath := u.Path != "" + u.Path, u.RawQuery, u.Fragment = "", "", "" + base := u.String() + + endpoint = gophercloud.NormalizeURL(endpoint) + base = gophercloud.NormalizeURL(base) + + if hadPath { + return &gophercloud.ProviderClient{ + IdentityBase: base, + IdentityEndpoint: endpoint, + }, nil + } + + return &gophercloud.ProviderClient{ + IdentityBase: base, + IdentityEndpoint: "", + }, nil +} + +// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and +// returns a Client instance that's ready to operate. +// It first queries the root identity endpoint to determine which versions of the identity service are supported, then chooses +// the most recent identity service available to proceed. +func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + client, err := NewClient(options.IdentityEndpoint) + if err != nil { + return nil, err + } + + err = Authenticate(client, options) + if err != nil { + return nil, err + } + return client, nil +} + +// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint. +func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + versions := []*utils.Version{ + &utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"}, + &utils.Version{ID: v30, Priority: 30, Suffix: "/v3/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v20: + return v2auth(client, endpoint, options) + case v30: + return v3auth(client, endpoint, options) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("Unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV2 explicitly authenticates against the identity v2 endpoint. +func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + return v2auth(client, "", options) +} + +func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error { + v2Client := NewIdentityV2(client) + if endpoint != "" { + v2Client.Endpoint = endpoint + } + + result := tokens2.Create(v2Client, tokens2.AuthOptions{AuthOptions: options}) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + if options.AllowReauth { + client.ReauthFunc = func() error { + client.TokenID = "" + return AuthenticateV2(client, options) + } + } + client.TokenID = token.ID + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V2EndpointURL(catalog, opts) + } + + return nil +} + +// AuthenticateV3 explicitly authenticates against the identity v3 service. +func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + return v3auth(client, "", options) +} + +func v3auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error { + // Override the generated service endpoint with the one returned by the version endpoint. + v3Client := NewIdentityV3(client) + if endpoint != "" { + v3Client.Endpoint = endpoint + } + + var scope *tokens3.Scope + if options.TenantID != "" { + scope = &tokens3.Scope{ + ProjectID: options.TenantID, + } + options.TenantID = "" + options.TenantName = "" + } else { + if options.TenantName != "" { + scope = &tokens3.Scope{ + ProjectName: options.TenantName, + DomainID: options.DomainID, + DomainName: options.DomainName, + } + options.TenantName = "" + } + } + + result := tokens3.Create(v3Client, options, scope) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + client.TokenID = token.ID + + if options.AllowReauth { + client.ReauthFunc = func() error { + return AuthenticateV3(client, options) + } + } + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V3EndpointURL(catalog, opts) + } + + return nil +} + +// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service. +func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient { + v2Endpoint := client.IdentityBase + "v2.0/" + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: v2Endpoint, + } +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service. +func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClient { + v3Endpoint := client.IdentityBase + "v3/" + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: v3Endpoint, + } +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("object-store") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("compute") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package. +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, + ResourceBase: url + "v2.0/", + }, nil +} + +// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("volume") + 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 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 +} + +// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service. +func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("orchestration") + 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/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client_test.go new file mode 100644 index 000000000000..257260c4e194 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/client_test.go @@ -0,0 +1,161 @@ +package openstack + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticatedClientV3(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + const ID = "0123456789" + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "stable", + "id": "v3.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + } + ] + } + } + `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/") + }) + + th.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Subject-Token", ID) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ "token": { "expires_at": "2013-02-02T18:30:59.000000Z" } }`) + }) + + options := gophercloud.AuthOptions{ + UserID: "me", + Password: "secret", + IdentityEndpoint: th.Endpoint(), + } + client, err := AuthenticatedClient(options) + th.AssertNoErr(t, err) + th.CheckEquals(t, ID, client.TokenID) +} + +func TestAuthenticatedClientV2(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "experimental", + "id": "v3.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + } + ] + } + } + `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/") + }) + + th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "access": { + "token": { + "id": "01234567890", + "expires": "2014-10-01T10:00:00.000000Z" + }, + "serviceCatalog": [ + { + "name": "Cloud Servers", + "type": "compute", + "endpoints": [ + { + "tenantId": "t1000", + "publicURL": "https://compute.north.host.com/v1/t1000", + "internalURL": "https://compute.north.internal/v1/t1000", + "region": "North", + "versionId": "1", + "versionInfo": "https://compute.north.host.com/v1/", + "versionList": "https://compute.north.host.com/" + }, + { + "tenantId": "t1000", + "publicURL": "https://compute.north.host.com/v1.1/t1000", + "internalURL": "https://compute.north.internal/v1.1/t1000", + "region": "North", + "versionId": "1.1", + "versionInfo": "https://compute.north.host.com/v1.1/", + "versionList": "https://compute.north.host.com/" + } + ], + "endpoints_links": [] + }, + { + "name": "Cloud Files", + "type": "object-store", + "endpoints": [ + { + "tenantId": "t1000", + "publicURL": "https://storage.north.host.com/v1/t1000", + "internalURL": "https://storage.north.internal/v1/t1000", + "region": "North", + "versionId": "1", + "versionInfo": "https://storage.north.host.com/v1/", + "versionList": "https://storage.north.host.com/" + }, + { + "tenantId": "t1000", + "publicURL": "https://storage.south.host.com/v1/t1000", + "internalURL": "https://storage.south.internal/v1/t1000", + "region": "South", + "versionId": "1", + "versionInfo": "https://storage.south.host.com/v1/", + "versionList": "https://storage.south.host.com/" + } + ] + } + ] + } + } + `) + }) + + options := gophercloud.AuthOptions{ + Username: "me", + Password: "secret", + IdentityEndpoint: th.Endpoint(), + } + client, err := AuthenticatedClient(options) + th.AssertNoErr(t, err) + th.CheckEquals(t, "01234567890", client.TokenID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/README.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/README.md new file mode 100644 index 000000000000..7b55795d08e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/README.md @@ -0,0 +1,3 @@ +# Common Resources + +This directory is for resources that are shared by multiple services. diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/doc.go new file mode 100644 index 000000000000..4a168f4b2c8a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/doc.go @@ -0,0 +1,15 @@ +// Package extensions provides information and interaction with the different extensions available +// for an OpenStack service. +// +// The purpose of OpenStack API extensions is to: +// +// - Introduce new features in the API without requiring a version change. +// - Introduce vendor-specific niche functionality. +// - Act as a proving ground for experimental functionalities that might be included in a future +// version of the API. +// +// Extensions usually have tags that prevent conflicts with other extensions that define attributes +// or resources with the same names, and with core resources and attributes. +// Because an extension might not be supported by all plug-ins, its availability varies with deployments +// and the specific plug-in. +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/errors.go new file mode 100644 index 000000000000..aeec0fa756e7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/errors.go @@ -0,0 +1 @@ +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/fixtures.go new file mode 100644 index 000000000000..0ed7de9f1d7b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/fixtures.go @@ -0,0 +1,91 @@ +// +build fixtures + +package extensions + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput provides a single page of Extension results. +const ListOutput = ` +{ + "extensions": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] +}` + +// GetOutput provides a single Extension result. +const GetOutput = ` +{ + "extension": { + "updated": "2013-02-03T10:00:00-00:00", + "name": "agent", + "links": [], + "namespace": "http://docs.openstack.org/ext/agent/api/v2.0", + "alias": "agent", + "description": "The agent management extension." + } +} +` + +// ListedExtension is the Extension that should be parsed from ListOutput. +var ListedExtension = Extension{ + Updated: "2013-01-20T00:00:00-00:00", + Name: "Neutron Service Type Management", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + Alias: "service-type", + Description: "API for retrieving service providers for Neutron advanced services", +} + +// ExpectedExtensions is a slice containing the Extension that should be parsed from ListOutput. +var ExpectedExtensions = []Extension{ListedExtension} + +// SingleExtension is the Extension that should be parsed from GetOutput. +var SingleExtension = &Extension{ + Updated: "2013-02-03T10:00:00-00:00", + Name: "agent", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/agent/api/v2.0", + Alias: "agent", + Description: "The agent management extension.", +} + +// HandleListExtensionsSuccessfully creates an HTTP handler at `/extensions` on the test handler +// mux that response with a list containing a single tenant. +func HandleListExtensionsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetExtensionSuccessfully creates an HTTP handler at `/extensions/agent` that responds with +// a JSON payload corresponding to SingleExtension. +func HandleGetExtensionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetOutput) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests.go new file mode 100644 index 000000000000..0b7108501563 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests.go @@ -0,0 +1,21 @@ +package extensions + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) GetResult { + var res GetResult + _, res.Err = c.Get(ExtensionURL(c, alias), &res.Body, nil) + return res +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, ListExtensionURL(c), func(r pagination.PageResult) pagination.Page { + return ExtensionPage{pagination.SinglePageBase(r)} + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests_test.go new file mode 100644 index 000000000000..6550283df71c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/requests_test.go @@ -0,0 +1,38 @@ +package extensions + +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() + HandleListExtensionsSuccessfully(t) + + count := 0 + + List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, ExpectedExtensions, actual) + + return true, nil + }) + + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetExtensionSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, SingleExtension, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/results.go new file mode 100644 index 000000000000..777d083fa077 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/results.go @@ -0,0 +1,65 @@ +package extensions + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// GetResult temporarily stores the result of a Get call. +// Use its Extract() method to interpret it as an Extension. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as an Extension. +func (r GetResult) Extract() (*Extension, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Extension *Extension `json:"extension"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Extension, err +} + +// Extension is a struct that represents an OpenStack extension. +type Extension struct { + Updated string `json:"updated" mapstructure:"updated"` + Name string `json:"name" mapstructure:"name"` + Links []interface{} `json:"links" mapstructure:"links"` + Namespace string `json:"namespace" mapstructure:"namespace"` + Alias string `json:"alias" mapstructure:"alias"` + Description string `json:"description" mapstructure:"description"` +} + +// ExtensionPage is the page returned by a pager when traversing over a collection of extensions. +type ExtensionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an ExtensionPage struct is empty. +func (r ExtensionPage) IsEmpty() (bool, error) { + is, err := ExtractExtensions(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the +// elements into a slice of Extension structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractExtensions(page pagination.Page) ([]Extension, error) { + var resp struct { + Extensions []Extension `mapstructure:"extensions"` + } + + err := mapstructure.Decode(page.(ExtensionPage).Body, &resp) + + return resp.Extensions, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls.go new file mode 100644 index 000000000000..6460c66bc01e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls.go @@ -0,0 +1,13 @@ +package extensions + +import "github.com/rackspace/gophercloud" + +// ExtensionURL generates the URL for an extension resource by name. +func ExtensionURL(c *gophercloud.ServiceClient, name string) string { + return c.ServiceURL("extensions", name) +} + +// ListExtensionURL generates the URL for the extensions resource collection. +func ListExtensionURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("extensions") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls_test.go new file mode 100644 index 000000000000..3223b1ca8b07 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/common/extensions/urls_test.go @@ -0,0 +1,26 @@ +package extensions + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestExtensionURL(t *testing.T) { + actual := ExtensionURL(endpointClient(), "agent") + expected := endpoint + "extensions/agent" + th.AssertEquals(t, expected, actual) +} + +func TestListExtensionURL(t *testing.T) { + actual := ListExtensionURL(endpointClient()) + expected := endpoint + "extensions" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go new file mode 100644 index 000000000000..c0ba368db759 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go @@ -0,0 +1,106 @@ +package bootfromvolume + +import ( + "errors" + "strconv" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// SourceType represents the type of medium being used to create the volume. +type SourceType string + +const ( + Volume SourceType = "volume" + Snapshot SourceType = "snapshot" + Image SourceType = "image" +) + +// BlockDevice is a structure with options for booting a server instance +// from a volume. The volume may be created from an image, snapshot, or another +// volume. +type BlockDevice struct { + // BootIndex [optional] is the boot index. It defaults to 0. + BootIndex int `json:"boot_index"` + + // DeleteOnTermination [optional] specifies whether or not to delete the attached volume + // when the server is deleted. Defaults to `false`. + DeleteOnTermination bool `json:"delete_on_termination"` + + // DestinationType [optional] is the type that gets created. Possible values are "volume" + // and "local". + DestinationType string `json:"destination_type"` + + // SourceType [required] must be one of: "volume", "snapshot", "image". + SourceType SourceType `json:"source_type"` + + // UUID [required] is the unique identifier for the volume, snapshot, or image (see above) + UUID string `json:"uuid"` + + // VolumeSize [optional] is the size of the volume to create (in gigabytes). + VolumeSize int `json:"volume_size"` +} + +// CreateOptsExt is a structure that extends the server `CreateOpts` structure +// by allowing for a block device mapping. +type CreateOptsExt struct { + servers.CreateOptsBuilder + BlockDevice []BlockDevice `json:"block_device_mapping_v2,omitempty"` +} + +// ToServerCreateMap adds the block device mapping option to the base server +// creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if len(opts.BlockDevice) == 0 { + return nil, errors.New("Required fields UUID and SourceType not set.") + } + + serverMap := base["server"].(map[string]interface{}) + + blockDevice := make([]map[string]interface{}, len(opts.BlockDevice)) + + for i, bd := range opts.BlockDevice { + if string(bd.SourceType) == "" { + return nil, errors.New("SourceType must be one of: volume, image, snapshot.") + } + + blockDevice[i] = make(map[string]interface{}) + + blockDevice[i]["source_type"] = bd.SourceType + blockDevice[i]["boot_index"] = strconv.Itoa(bd.BootIndex) + blockDevice[i]["delete_on_termination"] = strconv.FormatBool(bd.DeleteOnTermination) + blockDevice[i]["volume_size"] = strconv.Itoa(bd.VolumeSize) + if bd.UUID != "" { + blockDevice[i]["uuid"] = bd.UUID + } + if bd.DestinationType != "" { + blockDevice[i]["destination_type"] = bd.DestinationType + } + + } + serverMap["block_device_mapping_v2"] = blockDevice + + return base, nil +} + +// Create requests the creation of a server from the given block device mapping. +func Create(client *gophercloud.ServiceClient, opts servers.CreateOptsBuilder) servers.CreateResult { + var res servers.CreateResult + + reqBody, err := opts.ToServerCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go new file mode 100644 index 000000000000..5bf9137906c1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests_test.go @@ -0,0 +1,51 @@ +package bootfromvolume + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []BlockDevice{ + BlockDevice{ + UUID: "123456", + SourceType: Image, + DestinationType: "volume", + VolumeSize: 10, + }, + }, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"image", + "destination_type":"volume", + "boot_index": "0", + "delete_on_termination": "false", + "volume_size": "10" + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go new file mode 100644 index 000000000000..f60329f0f385 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go @@ -0,0 +1,10 @@ +package bootfromvolume + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// CreateResult temporarily contains the response from a Create call. +type CreateResult struct { + os.CreateResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go new file mode 100644 index 000000000000..0cffe25ffdcb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go @@ -0,0 +1,7 @@ +package bootfromvolume + +import "github.com/rackspace/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-volumes_boot") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls_test.go new file mode 100644 index 000000000000..6ee647732d41 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls_test.go @@ -0,0 +1,16 @@ +package bootfromvolume + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestCreateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-volumes_boot", createURL(c)) +} 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 000000000000..2571a1a5a779 --- /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 000000000000..c28e492d357a --- /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 000000000000..9f27ef172c19 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules/requests.go @@ -0,0 +1,95 @@ +package defsecrules + +import ( + "errors" + + "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 = client.Post(rootURL(client), reqBody, &result.Body, &gophercloud.RequestOpts{ + 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 = client.Get(resourceURL(client, id), &result.Body, nil) + 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 = client.Delete(resourceURL(client, id), nil) + 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 000000000000..d4ebe87c5641 --- /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 000000000000..e588d3e32763 --- /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 000000000000..cc928ab8952a --- /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/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate.go new file mode 100644 index 000000000000..10079097b62a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate.go @@ -0,0 +1,23 @@ +package extensions + +import ( + "github.com/rackspace/gophercloud" + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractExtensions interprets a Page as a slice of Extensions. +func ExtractExtensions(page pagination.Page) ([]common.Extension, error) { + return common.ExtractExtensions(page) +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) common.GetResult { + return common.Get(c, alias) +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate_test.go new file mode 100644 index 000000000000..c3c525fa20ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/delegate_test.go @@ -0,0 +1,96 @@ +package extensions + +import ( + "fmt" + "net/http" + "testing" + + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "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() + + th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ` +{ + "extensions": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] +} + `) + }) + + count := 0 + List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + th.AssertNoErr(t, err) + + expected := []common.Extension{ + common.Extension{ + Updated: "2013-01-20T00:00:00-00:00", + Name: "Neutron Service Type Management", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + Alias: "service-type", + Description: "API for retrieving service providers for Neutron advanced services", + }, + } + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "extension": { + "updated": "2013-02-03T10:00:00-00:00", + "name": "agent", + "links": [], + "namespace": "http://docs.openstack.org/ext/agent/api/v2.0", + "alias": "agent", + "description": "The agent management extension." + } +} + `) + }) + + ext, err := Get(client.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00") + th.AssertEquals(t, ext.Name, "agent") + th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0") + th.AssertEquals(t, ext.Alias, "agent") + th.AssertEquals(t, ext.Description, "The agent management extension.") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go new file mode 100644 index 000000000000..80785faca9fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/doc.go @@ -0,0 +1,3 @@ +// Package diskconfig provides information and interaction with the Disk +// Config extension that works with the OpenStack Compute service. +package diskconfig diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go new file mode 100644 index 000000000000..7407e0d175ee --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests.go @@ -0,0 +1,114 @@ +package diskconfig + +import ( + "errors" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// DiskConfig represents one of the two possible settings for the DiskConfig option when creating, +// rebuilding, or resizing servers: Auto or Manual. +type DiskConfig string + +const ( + // Auto builds a server with a single partition the size of the target flavor disk and + // automatically adjusts the filesystem to fit the entire partition. Auto may only be used with + // images and servers that use a single EXT3 partition. + Auto DiskConfig = "AUTO" + + // Manual builds a server using whatever partition scheme and filesystem are present in the source + // image. If the target flavor disk is larger, the remaining space is left unpartitioned. This + // enables images to have non-EXT3 filesystems, multiple partitions, and so on, and enables you + // to manage the disk configuration. It also results in slightly shorter boot times. + Manual DiskConfig = "MANUAL" +) + +// ErrInvalidDiskConfig is returned if an invalid string is specified for a DiskConfig option. +var ErrInvalidDiskConfig = errors.New("DiskConfig must be either diskconfig.Auto or diskconfig.Manual.") + +// Validate ensures that a DiskConfig contains an appropriate value. +func (config DiskConfig) validate() error { + switch config { + case Auto, Manual: + return nil + default: + return ErrInvalidDiskConfig + } +} + +// CreateOptsExt adds a DiskConfig option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // DiskConfig [optional] controls how the created server's disk is partitioned. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` +} + +// ToServerCreateMap adds the diskconfig option to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if string(opts.DiskConfig) == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} + +// RebuildOptsExt adds a DiskConfig option to the base RebuildOpts. +type RebuildOptsExt struct { + servers.RebuildOptsBuilder + + // DiskConfig [optional] controls how the rebuilt server's disk is partitioned. + DiskConfig DiskConfig +} + +// ToServerRebuildMap adds the diskconfig option to the base server rebuild options. +func (opts RebuildOptsExt) ToServerRebuildMap() (map[string]interface{}, error) { + err := opts.DiskConfig.validate() + if err != nil { + return nil, err + } + + base, err := opts.RebuildOptsBuilder.ToServerRebuildMap() + if err != nil { + return nil, err + } + + serverMap := base["rebuild"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} + +// ResizeOptsExt adds a DiskConfig option to the base server resize options. +type ResizeOptsExt struct { + servers.ResizeOptsBuilder + + // DiskConfig [optional] controls how the resized server's disk is partitioned. + DiskConfig DiskConfig +} + +// ToServerResizeMap adds the diskconfig option to the base server creation options. +func (opts ResizeOptsExt) ToServerResizeMap() (map[string]interface{}, error) { + err := opts.DiskConfig.validate() + if err != nil { + return nil, err + } + + base, err := opts.ResizeOptsBuilder.ToServerResizeMap() + if err != nil { + return nil, err + } + + serverMap := base["resize"].(map[string]interface{}) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) + + return base, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go new file mode 100644 index 000000000000..e3c26d49a552 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/requests_test.go @@ -0,0 +1,87 @@ +package diskconfig + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := CreateOptsExt{ + CreateOptsBuilder: base, + DiskConfig: Manual, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "OS-DCF:diskConfig": "MANUAL" + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestRebuildOpts(t *testing.T) { + base := servers.RebuildOpts{ + Name: "rebuiltserver", + AdminPass: "swordfish", + ImageID: "asdfasdfasdf", + } + + ext := RebuildOptsExt{ + RebuildOptsBuilder: base, + DiskConfig: Auto, + } + + actual, err := ext.ToServerRebuildMap() + th.AssertNoErr(t, err) + + expected := ` + { + "rebuild": { + "name": "rebuiltserver", + "imageRef": "asdfasdfasdf", + "adminPass": "swordfish", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} + +func TestResizeOpts(t *testing.T) { + base := servers.ResizeOpts{ + FlavorRef: "performance1-8", + } + + ext := ResizeOptsExt{ + ResizeOptsBuilder: base, + DiskConfig: Auto, + } + + actual, err := ext.ToServerResizeMap() + th.AssertNoErr(t, err) + + expected := ` + { + "resize": { + "flavorRef": "performance1-8", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go new file mode 100644 index 000000000000..10ec2dafcb8d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results.go @@ -0,0 +1,60 @@ +package diskconfig + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" +) + +func commonExtract(result gophercloud.Result) (*DiskConfig, error) { + var resp struct { + Server struct { + DiskConfig string `mapstructure:"OS-DCF:diskConfig"` + } `mapstructure:"server"` + } + + err := mapstructure.Decode(result.Body, &resp) + if err != nil { + return nil, err + } + + config := DiskConfig(resp.Server.DiskConfig) + return &config, nil +} + +// ExtractGet returns the disk configuration from a servers.Get call. +func ExtractGet(result servers.GetResult) (*DiskConfig, error) { + return commonExtract(result.Result) +} + +// ExtractUpdate returns the disk configuration from a servers.Update call. +func ExtractUpdate(result servers.UpdateResult) (*DiskConfig, error) { + return commonExtract(result.Result) +} + +// ExtractRebuild returns the disk configuration from a servers.Rebuild call. +func ExtractRebuild(result servers.RebuildResult) (*DiskConfig, error) { + return commonExtract(result.Result) +} + +// ExtractDiskConfig returns the DiskConfig setting for a specific server acquired from an +// servers.ExtractServers call, while iterating through a Pager. +func ExtractDiskConfig(page pagination.Page, index int) (*DiskConfig, error) { + casted := page.(servers.ServerPage).Body + + type server struct { + DiskConfig string `mapstructure:"OS-DCF:diskConfig"` + } + var response struct { + Servers []server `mapstructure:"servers"` + } + + err := mapstructure.Decode(casted, &response) + if err != nil { + return nil, err + } + + config := DiskConfig(response.Servers[index].DiskConfig) + return &config, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results_test.go new file mode 100644 index 000000000000..dd8d2b7dfa75 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig/results_test.go @@ -0,0 +1,68 @@ +package diskconfig + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestExtractGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleServerGetSuccessfully(t) + + config, err := ExtractGet(servers.Get(client.ServiceClient(), "1234asdf")) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) +} + +func TestExtractUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleServerUpdateSuccessfully(t) + + r := servers.Update(client.ServiceClient(), "1234asdf", servers.UpdateOpts{ + Name: "new-name", + }) + config, err := ExtractUpdate(r) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) +} + +func TestExtractRebuild(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleRebuildSuccessfully(t, servers.SingleServerBody) + + r := servers.Rebuild(client.ServiceClient(), "1234asdf", servers.RebuildOpts{ + Name: "new-name", + AdminPass: "swordfish", + ImageID: "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + AccessIPv4: "1.2.3.4", + }) + config, err := ExtractRebuild(r) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) +} + +func TestExtractList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + servers.HandleServerListSuccessfully(t) + + pages := 0 + err := servers.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + config, err := ExtractDiskConfig(page, 0) + th.AssertNoErr(t, err) + th.CheckEquals(t, Manual, *config) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, pages, 1) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/doc.go new file mode 100644 index 000000000000..2b447da1d658 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the +// different extensions available for the OpenStack Compute service. +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/doc.go new file mode 100644 index 000000000000..f74f58ce8377 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/doc.go @@ -0,0 +1,3 @@ +// Package floatingip provides the ability to manage floating ips through +// nova-network +package floatingip diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/fixtures.go new file mode 100644 index 000000000000..26f32995fdeb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/fixtures.go @@ -0,0 +1,174 @@ +// +build fixtures + +package floatingip + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "floating_ips": [ + { + "fixed_ip": null, + "id": 1, + "instance_id": null, + "ip": "10.10.10.1", + "pool": "nova" + }, + { + "fixed_ip": "166.78.185.201", + "id": 2, + "instance_id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "ip": "10.10.10.2", + "pool": "nova" + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "floating_ip": { + "fixed_ip": "166.78.185.201", + "id": 2, + "instance_id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "ip": "10.10.10.2", + "pool": "nova" + } +} +` + +// CreateOutput is a sample response to a Post call +const CreateOutput = ` +{ + "floating_ip": { + "fixed_ip": null, + "id": 1, + "instance_id": null, + "ip": "10.10.10.1", + "pool": "nova" + } +} +` + +// FirstFloatingIP is the first result in ListOutput. +var FirstFloatingIP = FloatingIP{ + ID: "1", + IP: "10.10.10.1", + Pool: "nova", +} + +// SecondFloatingIP is the first result in ListOutput. +var SecondFloatingIP = FloatingIP{ + FixedIP: "166.78.185.201", + ID: "2", + InstanceID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + IP: "10.10.10.2", + Pool: "nova", +} + +// ExpectedFloatingIPsSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedFloatingIPsSlice = []FloatingIP{FirstFloatingIP, SecondFloatingIP} + +// CreatedFloatingIP is the parsed result from CreateOutput. +var CreatedFloatingIP = FloatingIP{ + ID: "1", + IP: "10.10.10.1", + Pool: "nova", +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing floating ip +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips/2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request +// for a new floating ip +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "pool": "nova" +} +`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// an existing floating ip +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-floating-ips/1", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleAssociateSuccessfully configures the test server to respond to a Post request +// to associate an allocated floating IP +func HandleAssociateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "addFloatingIp": { + "address": "10.10.10.2" + } +} +`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleDisassociateSuccessfully configures the test server to respond to a Post request +// to disassociate an allocated floating IP +func HandleDisassociateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "removeFloatingIp": { + "address": "10.10.10.2" + } +} +`) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/requests.go new file mode 100644 index 000000000000..8abb72dcdec7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/requests.go @@ -0,0 +1,92 @@ +package floatingip + +import ( + "errors" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of FloatingIPs. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return FloatingIPsPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToFloatingIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies a Floating IP allocation request +type CreateOpts struct { + // Pool is the pool of floating IPs to allocate one from + Pool string +} + +// ToFloatingIPCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) { + if opts.Pool == "" { + return nil, errors.New("Missing field required for floating IP creation: Pool") + } + + return map[string]interface{}{"pool": opts.Pool}, nil +} + +// Create requests the creation of a new floating IP +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToFloatingIPCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Get returns data about a previously created FloatingIP. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res +} + +// Delete requests the deletion of a previous allocated FloatingIP. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(deleteURL(client, id), nil) + return res +} + +// association / disassociation + +// Associate pairs an allocated floating IP with an instance +func Associate(client *gophercloud.ServiceClient, serverId, fip string) AssociateResult { + var res AssociateResult + + addFloatingIp := make(map[string]interface{}) + addFloatingIp["address"] = fip + reqBody := map[string]interface{}{"addFloatingIp": addFloatingIp} + + _, res.Err = client.Post(associateURL(client, serverId), reqBody, nil, nil) + return res +} + +// Disassociate decouples an allocated floating IP from an instance +func Disassociate(client *gophercloud.ServiceClient, serverId, fip string) DisassociateResult { + var res DisassociateResult + + removeFloatingIp := make(map[string]interface{}) + removeFloatingIp["address"] = fip + reqBody := map[string]interface{}{"removeFloatingIp": removeFloatingIp} + + _, res.Err = client.Post(disassociateURL(client, serverId), reqBody, nil, nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/requests_test.go new file mode 100644 index 000000000000..ed2460edc674 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/requests_test.go @@ -0,0 +1,80 @@ +package floatingip + +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() + HandleListSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractFloatingIPs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedFloatingIPsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Pool: "nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedFloatingIP, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "2").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondFloatingIP, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := Delete(client.ServiceClient(), "1").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAssociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAssociateSuccessfully(t) + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + fip := "10.10.10.2" + + err := Associate(client.ServiceClient(), serverId, fip).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDisassociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDisassociateSuccessfully(t) + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + fip := "10.10.10.2" + + err := Disassociate(client.ServiceClient(), serverId, fip).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/results.go new file mode 100644 index 000000000000..be77fa17922c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/results.go @@ -0,0 +1,99 @@ +package floatingip + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// A FloatingIP is an IP that can be associated with an instance +type FloatingIP struct { + // ID is a unique ID of the Floating IP + ID string `mapstructure:"id"` + + // FixedIP is the IP of the instance related to the Floating IP + FixedIP string `mapstructure:"fixed_ip,omitempty"` + + // InstanceID is the ID of the instance that is using the Floating IP + InstanceID string `mapstructure:"instance_id"` + + // IP is the actual Floating IP + IP string `mapstructure:"ip"` + + // Pool is the pool of floating IPs that this floating IP belongs to + Pool string `mapstructure:"pool"` +} + +// FloatingIPsPage stores a single, only page of FloatingIPs +// results from a List call. +type FloatingIPsPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a FloatingIPsPage is empty. +func (page FloatingIPsPage) IsEmpty() (bool, error) { + va, err := ExtractFloatingIPs(page) + return len(va) == 0, err +} + +// ExtractFloatingIPs interprets a page of results as a slice of +// FloatingIPs. +func ExtractFloatingIPs(page pagination.Page) ([]FloatingIP, error) { + casted := page.(FloatingIPsPage).Body + var response struct { + FloatingIPs []FloatingIP `mapstructure:"floating_ips"` + } + + err := mapstructure.WeakDecode(casted, &response) + + return response.FloatingIPs, err +} + +type FloatingIPResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any FloatingIP resource +// response as a FloatingIP struct. +func (r FloatingIPResult) Extract() (*FloatingIP, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + FloatingIP *FloatingIP `json:"floating_ip" mapstructure:"floating_ip"` + } + + err := mapstructure.WeakDecode(r.Body, &res) + return res.FloatingIP, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a FloatingIP. +type CreateResult struct { + FloatingIPResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a FloatingIP. +type GetResult struct { + FloatingIPResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AssociateResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type AssociateResult struct { + gophercloud.ErrResult +} + +// DisassociateResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DisassociateResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/urls.go new file mode 100644 index 000000000000..54198f852916 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/urls.go @@ -0,0 +1,37 @@ +package floatingip + +import "github.com/rackspace/gophercloud" + +const resourcePath = "os-floating-ips" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} + +func serverURL(c *gophercloud.ServiceClient, serverId string) string { + return c.ServiceURL("servers/" + serverId + "/action") +} + +func associateURL(c *gophercloud.ServiceClient, serverId string) string { + return serverURL(c, serverId) +} + +func disassociateURL(c *gophercloud.ServiceClient, serverId string) string { + return serverURL(c, serverId) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/urls_test.go new file mode 100644 index 000000000000..f73d6fb0f9fe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip/urls_test.go @@ -0,0 +1,60 @@ +package floatingip + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-floating-ips", listURL(c)) +} + +func TestCreateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-floating-ips", createURL(c)) +} + +func TestGetURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + id := "1" + + th.CheckEquals(t, c.Endpoint+"os-floating-ips/"+id, getURL(c, id)) +} + +func TestDeleteURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + id := "1" + + th.CheckEquals(t, c.Endpoint+"os-floating-ips/"+id, deleteURL(c, id)) +} + +func TestAssociateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/action", associateURL(c, serverId)) +} + +func TestDisassociateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/action", disassociateURL(c, serverId)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go new file mode 100644 index 000000000000..856f41bacc98 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go @@ -0,0 +1,3 @@ +// Package keypairs provides information and interaction with the Keypairs +// extension for the OpenStack Compute service. +package keypairs diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/fixtures.go new file mode 100644 index 000000000000..d10af99d0eb9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/fixtures.go @@ -0,0 +1,171 @@ +// +build fixtures + +package keypairs + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "keypairs": [ + { + "keypair": { + "fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + "name": "firstkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n" + } + }, + { + "keypair": { + "fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + "name": "secondkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n" + } + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "keypair": { + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", + "name": "firstkey", + "fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a" + } +} +` + +// CreateOutput is a sample response to a Create call. +const CreateOutput = ` +{ + "keypair": { + "fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + "name": "createdkey", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + "user_id": "fake" + } +} +` + +// ImportOutput is a sample response to a Create call that provides its own public key. +const ImportOutput = ` +{ + "keypair": { + "fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + "user_id": "fake" + } +} +` + +// FirstKeyPair is the first result in ListOutput. +var FirstKeyPair = KeyPair{ + Name: "firstkey", + Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n", +} + +// SecondKeyPair is the second result in ListOutput. +var SecondKeyPair = KeyPair{ + Name: "secondkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", +} + +// ExpectedKeyPairSlice is the slice of results that should be parsed from ListOutput, in the expected +// order. +var ExpectedKeyPairSlice = []KeyPair{FirstKeyPair, SecondKeyPair} + +// CreatedKeyPair is the parsed result from CreatedOutput. +var CreatedKeyPair = KeyPair{ + Name: "createdkey", + Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n", + UserID: "fake", +} + +// ImportedKeyPair is the parsed result from ImportOutput. +var ImportedKeyPair = KeyPair{ + Name: "importedkey", + Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + UserID: "fake", +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request for "firstkey". +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs/firstkey", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request for a new +// keypair called "createdkey". +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "keypair": { "name": "createdkey" } }`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleImportSuccessfully configures the test server to respond to an Import request for an +// existing keypair called "importedkey". +func HandleImportSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "keypair": { + "name": "importedkey", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ImportOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// keypair called "deletedkey". +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-keypairs/deletedkey", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/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 new file mode 100644 index 000000000000..c56ee67ea2d2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go @@ -0,0 +1,102 @@ +package keypairs + +import ( + "errors" + + "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 { + return KeyPairPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToKeyPairCreateMap() (map[string]interface{}, error) +} + +// 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 + + // PublicKey [optional] is a pregenerated OpenSSH-formatted public key. If provided, this key + // will be imported and no new key will be created. + PublicKey string +} + +// ToKeyPairCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) { + if opts.Name == "" { + return nil, errors.New("Missing field required for keypair creation: Name") + } + + keypair := make(map[string]interface{}) + keypair["name"] = opts.Name + if opts.PublicKey != "" { + keypair["public_key"] = opts.PublicKey + } + + return map[string]interface{}{"keypair": keypair}, nil +} + +// Create requests the creation of a new keypair on the server, or to import a pre-existing +// keypair. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToKeyPairCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *gophercloud.ServiceClient, name string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, name), &res.Body, nil) + return res +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *gophercloud.ServiceClient, name string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(deleteURL(client, name), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests_test.go new file mode 100644 index 000000000000..67d1833f5727 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/requests_test.go @@ -0,0 +1,71 @@ +package keypairs + +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() + HandleListSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedKeyPair, actual) +} + +func TestImport(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleImportSuccessfully(t) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &ImportedKeyPair, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstKeyPair, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := Delete(client.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/results.go new file mode 100644 index 000000000000..f1a0d8e114c9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/results.go @@ -0,0 +1,94 @@ +package keypairs + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// KeyPair is an SSH key known to the OpenStack cluster that is available to be injected into +// servers. +type KeyPair struct { + // Name is used to refer to this keypair from other services within this region. + Name string `mapstructure:"name"` + + // Fingerprint is a short sequence of bytes that can be used to authenticate or validate a longer + // public key. + Fingerprint string `mapstructure:"fingerprint"` + + // PublicKey is the public key from this pair, in OpenSSH format. "ssh-rsa AAAAB3Nz..." + PublicKey string `mapstructure:"public_key"` + + // PrivateKey is the private key from this pair, in PEM format. + // "-----BEGIN RSA PRIVATE KEY-----\nMIICXA..." It is only present if this keypair was just + // returned from a Create call + PrivateKey string `mapstructure:"private_key"` + + // UserID is the user who owns this keypair. + UserID string `mapstructure:"user_id"` +} + +// KeyPairPage stores a single, only page of KeyPair results from a List call. +type KeyPairPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a KeyPairPage is empty. +func (page KeyPairPage) IsEmpty() (bool, error) { + ks, err := ExtractKeyPairs(page) + return len(ks) == 0, err +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(page pagination.Page) ([]KeyPair, error) { + type pair struct { + KeyPair KeyPair `mapstructure:"keypair"` + } + + var resp struct { + KeyPairs []pair `mapstructure:"keypairs"` + } + + err := mapstructure.Decode(page.(KeyPairPage).Body, &resp) + results := make([]KeyPair, len(resp.KeyPairs)) + for i, pair := range resp.KeyPairs { + results[i] = pair.KeyPair + } + return results, err +} + +type keyPairResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any KeyPair resource response as a KeyPair struct. +func (r keyPairResult) Extract() (*KeyPair, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + KeyPair *KeyPair `json:"keypair" mapstructure:"keypair"` + } + + err := mapstructure.Decode(r.Body, &res) + return res.KeyPair, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a KeyPair. +type CreateResult struct { + keyPairResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a KeyPair. +type GetResult struct { + keyPairResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go new file mode 100644 index 000000000000..702f5329e05c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go @@ -0,0 +1,25 @@ +package keypairs + +import "github.com/rackspace/gophercloud" + +const resourcePath = "os-keypairs" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, name string) string { + return c.ServiceURL(resourcePath, name) +} + +func deleteURL(c *gophercloud.ServiceClient, name string) string { + return getURL(c, name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls_test.go new file mode 100644 index 000000000000..60efd2a5d333 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs/urls_test.go @@ -0,0 +1,40 @@ +package keypairs + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs", listURL(c)) +} + +func TestCreateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs", createURL(c)) +} + +func TestGetURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs/wat", getURL(c, "wat")) +} + +func TestDeleteURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-keypairs/wat", deleteURL(c, "wat")) +} 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 000000000000..702f32c9854c --- /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 000000000000..1c6ba394920f --- /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("/servers/%s%s", serverID, rootPath) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, listGroupsJSON) + }) +} + +func mockCreateGroupResponse(t *testing.T) { + th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group": { + "name": "test", + "description": "something" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "something", + "id": "{groupID}", + "name": "test", + "rules": [], + "tenant_id": "openstack" + } +} +`) + }) +} + +func mockUpdateGroupResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + th.TestJSONRequest(t, r, ` +{ + "security_group": { + "name": "new_name", + "description": "new_desc" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "something", + "id": "{groupID}", + "name": "new_name", + "rules": [], + "tenant_id": "openstack" + } +} +`) + }) +} + +func mockGetGroupsResponse(t *testing.T, groupID string) { + url := fmt.Sprintf("%s/%s", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "default", + "id": "{groupID}", + "name": "default", + "rules": [ + { + "from_port": 80, + "group": { + "tenant_id": "openstack", + "name": "default" + }, + "ip_protocol": "TCP", + "to_port": 85, + "parent_group_id": "{groupID}", + "ip_range": { + "cidr": "0.0.0.0" + }, + "id": "{ruleID}" + } + ], + "tenant_id": "openstack" + } +} + `) + }) +} + +func mockGetNumericIDGroupResponse(t *testing.T, groupID int) { + url := fmt.Sprintf("%s/%d", rootPath, groupID) + th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "security_group": { + "id": 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 000000000000..4cef48022236 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go @@ -0,0 +1,257 @@ +package secgroups + +import ( + "errors" + + "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 = client.Post(rootURL(client), reqBody, &result.Body, &gophercloud.RequestOpts{ + 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 = client.Put(resourceURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{ + 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 = client.Get(resourceURL(client, id), &result.Body, nil) + 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 = client.Delete(resourceURL(client, id), nil) + 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["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 = client.Post(rootRuleURL(client), reqBody, &result.Body, &gophercloud.RequestOpts{ + 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 = client.Delete(resourceRuleURL(client, id), nil) + 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 = client.Post(serverActionURL(client, serverID), actionMap("add", groupName), &result.Body, nil) + 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 = client.Post(serverActionURL(client, serverID), actionMap("remove", groupName), &result.Body, nil) + 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 000000000000..4e21d5deaa14 --- /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 000000000000..478c5dc0973f --- /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 000000000000..dc53fbfac63a --- /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("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/servergroups/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/doc.go new file mode 100644 index 000000000000..1e5ed568daa3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/doc.go @@ -0,0 +1,2 @@ +// Package servergroups provides the ability to manage server groups +package servergroups diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/fixtures.go new file mode 100644 index 000000000000..133fd85ced18 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/fixtures.go @@ -0,0 +1,161 @@ +// +build fixtures + +package servergroups + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "server_groups": [ + { + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "name": "test", + "policies": [ + "anti-affinity" + ], + "members": [], + "metadata": {} + }, + { + "id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "name": "test2", + "policies": [ + "affinity" + ], + "members": [], + "metadata": {} + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "server_group": { + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "name": "test", + "policies": [ + "anti-affinity" + ], + "members": [], + "metadata": {} + } +} +` + +// CreateOutput is a sample response to a Post call +const CreateOutput = ` +{ + "server_group": { + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "name": "test", + "policies": [ + "anti-affinity" + ], + "members": [], + "metadata": {} + } +} +` + +// FirstServerGroup is the first result in ListOutput. +var FirstServerGroup = ServerGroup{ + ID: "616fb98f-46ca-475e-917e-2563e5a8cd19", + Name: "test", + Policies: []string{ + "anti-affinity", + }, + Members: []string{}, + Metadata: map[string]interface{}{}, +} + +// SecondServerGroup is the second result in ListOutput. +var SecondServerGroup = ServerGroup{ + ID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + Name: "test2", + Policies: []string{ + "affinity", + }, + Members: []string{}, + Metadata: map[string]interface{}{}, +} + +// ExpectedServerGroupSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedServerGroupSlice = []ServerGroup{FirstServerGroup, SecondServerGroup} + +// CreatedServerGroup is the parsed result from CreateOutput. +var CreatedServerGroup = ServerGroup{ + ID: "616fb98f-46ca-475e-917e-2563e5a8cd19", + Name: "test", + Policies: []string{ + "anti-affinity", + }, + Members: []string{}, + Metadata: map[string]interface{}{}, +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-server-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing server group +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-server-groups/4d8c3732-a248-40ed-bebc-539a6ffd25c0", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request +// for a new server group +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-server-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "server_group": { + "name": "test", + "policies": [ + "anti-affinity" + ] + } +} +`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// an existing server group +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-server-groups/616fb98f-46ca-475e-917e-2563e5a8cd19", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/requests.go new file mode 100644 index 000000000000..1597b43eb224 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/requests.go @@ -0,0 +1,77 @@ +package servergroups + +import ( + "errors" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of ServerGroups. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return ServerGroupsPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notably, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToServerGroupCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies a Server Group allocation request +type CreateOpts struct { + // Name is the name of the server group + Name string + + // Policies are the server group policies + Policies []string +} + +// ToServerGroupCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToServerGroupCreateMap() (map[string]interface{}, error) { + if opts.Name == "" { + return nil, errors.New("Missing field required for server group creation: Name") + } + + if len(opts.Policies) < 1 { + return nil, errors.New("Missing field required for server group creation: Policies") + } + + serverGroup := make(map[string]interface{}) + serverGroup["name"] = opts.Name + serverGroup["policies"] = opts.Policies + + return map[string]interface{}{"server_group": serverGroup}, nil +} + +// Create requests the creation of a new Server Group +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToServerGroupCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Get returns data about a previously created ServerGroup. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res +} + +// Delete requests the deletion of a previously allocated ServerGroup. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(deleteURL(client, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/requests_test.go new file mode 100644 index 000000000000..07fec51b1b8f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/requests_test.go @@ -0,0 +1,59 @@ +package servergroups + +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() + HandleListSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractServerGroups(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedServerGroupSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Name: "test", + Policies: []string{"anti-affinity"}, + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedServerGroup, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &FirstServerGroup, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := Delete(client.ServiceClient(), "616fb98f-46ca-475e-917e-2563e5a8cd19").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/results.go new file mode 100644 index 000000000000..d74ee5dbb000 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/results.go @@ -0,0 +1,87 @@ +package servergroups + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// A ServerGroup creates a policy for instance placement in the cloud +type ServerGroup struct { + // ID is the unique ID of the Server Group. + ID string `mapstructure:"id"` + + // Name is the common name of the server group. + Name string `mapstructure:"name"` + + // Polices are the group policies. + Policies []string `mapstructure:"policies"` + + // Members are the members of the server group. + Members []string `mapstructure:"members"` + + // Metadata includes a list of all user-specified key-value pairs attached to the Server Group. + Metadata map[string]interface{} +} + +// ServerGroupsPage stores a single, only page of ServerGroups +// results from a List call. +type ServerGroupsPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a ServerGroupsPage is empty. +func (page ServerGroupsPage) IsEmpty() (bool, error) { + va, err := ExtractServerGroups(page) + return len(va) == 0, err +} + +// ExtractServerGroups interprets a page of results as a slice of +// ServerGroups. +func ExtractServerGroups(page pagination.Page) ([]ServerGroup, error) { + casted := page.(ServerGroupsPage).Body + var response struct { + ServerGroups []ServerGroup `mapstructure:"server_groups"` + } + + err := mapstructure.WeakDecode(casted, &response) + + return response.ServerGroups, err +} + +type ServerGroupResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any Server Group resource +// response as a ServerGroup struct. +func (r ServerGroupResult) Extract() (*ServerGroup, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + ServerGroup *ServerGroup `json:"server_group" mapstructure:"server_group"` + } + + err := mapstructure.WeakDecode(r.Body, &res) + return res.ServerGroup, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a ServerGroup. +type CreateResult struct { + ServerGroupResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a ServerGroup. +type GetResult struct { + ServerGroupResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/urls.go new file mode 100644 index 000000000000..074a16c67f20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/urls.go @@ -0,0 +1,25 @@ +package servergroups + +import "github.com/rackspace/gophercloud" + +const resourcePath = "os-server-groups" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/urls_test.go new file mode 100644 index 000000000000..bff4dfc7205f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups/urls_test.go @@ -0,0 +1,42 @@ +package servergroups + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-server-groups", listURL(c)) +} + +func TestCreateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-server-groups", createURL(c)) +} + +func TestGetURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + id := "1" + + th.CheckEquals(t, c.Endpoint+"os-server-groups/"+id, getURL(c, id)) +} + +func TestDeleteURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + id := "1" + + th.CheckEquals(t, c.Endpoint+"os-server-groups/"+id, deleteURL(c, id)) +} 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 000000000000..d2729f874373 --- /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 000000000000..670828a986db --- /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 000000000000..0e090e69f26b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop/requests.go @@ -0,0 +1,23 @@ +package startstop + +import "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 = client.Post(actionURL(client, id), reqBody, nil, nil) + 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 = client.Post(actionURL(client, id), reqBody, nil, nil) + 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 000000000000..97a121b19aa0 --- /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/extensions/tenantnetworks/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/doc.go new file mode 100644 index 000000000000..65c46ff5078f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/doc.go @@ -0,0 +1,2 @@ +// Package tenantnetworks provides the ability for tenants to see information about the networks they have access to +package tenantnetworks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/fixtures.go new file mode 100644 index 000000000000..0cfa72ab0613 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/fixtures.go @@ -0,0 +1,84 @@ +// +build fixtures + +package tenantnetworks + +import ( + "fmt" + "net/http" + "testing" + "time" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "networks": [ + { + "cidr": "10.0.0.0/29", + "id": "20c8acc0-f747-4d71-a389-46d078ebf047", + "label": "mynet_0" + }, + { + "cidr": "10.0.0.10/29", + "id": "20c8acc0-f747-4d71-a389-46d078ebf000", + "label": "mynet_1" + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "network": { + "cidr": "10.0.0.10/29", + "id": "20c8acc0-f747-4d71-a389-46d078ebf000", + "label": "mynet_1" + } +} +` + +// FirstNetwork is the first result in ListOutput. +var nilTime time.Time +var FirstNetwork = Network{ + CIDR: "10.0.0.0/29", + ID: "20c8acc0-f747-4d71-a389-46d078ebf047", + Name: "mynet_0", +} + +// SecondNetwork is the second result in ListOutput. +var SecondNetwork = Network{ + CIDR: "10.0.0.10/29", + ID: "20c8acc0-f747-4d71-a389-46d078ebf000", + Name: "mynet_1", +} + +// ExpectedNetworkSlice is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedNetworkSlice = []Network{FirstNetwork, SecondNetwork} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-tenant-networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing network. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-tenant-networks/20c8acc0-f747-4d71-a389-46d078ebf000", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/requests.go new file mode 100644 index 000000000000..3ec13d384b0d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/requests.go @@ -0,0 +1,22 @@ +package tenantnetworks + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of Network. +func List(client *gophercloud.ServiceClient) pagination.Pager { + url := listURL(client) + createPage := func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, url, createPage) +} + +// Get returns data about a previously created Network. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/requests_test.go new file mode 100644 index 000000000000..fc4ee4f4bab0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/requests_test.go @@ -0,0 +1,37 @@ +package tenantnetworks + +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() + HandleListSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNetworks(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedNetworkSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "20c8acc0-f747-4d71-a389-46d078ebf000").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondNetwork, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/results.go new file mode 100644 index 000000000000..805009247a9c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/results.go @@ -0,0 +1,68 @@ +package tenantnetworks + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// A Network represents a nova-network that an instance communicates on +type Network struct { + // CIDR is the IPv4 subnet. + CIDR string `mapstructure:"cidr"` + + // ID is the UUID of the network. + ID string `mapstructure:"id"` + + // Name is the common name that the network has. + Name string `mapstructure:"label"` +} + +// NetworkPage stores a single, only page of Networks +// results from a List call. +type NetworkPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a NetworkPage is empty. +func (page NetworkPage) IsEmpty() (bool, error) { + va, err := ExtractNetworks(page) + return len(va) == 0, err +} + +// ExtractNetworks interprets a page of results as a slice of Networks +func ExtractNetworks(page pagination.Page) ([]Network, error) { + networks := page.(NetworkPage).Body + var res struct { + Networks []Network `mapstructure:"networks"` + } + + err := mapstructure.WeakDecode(networks, &res) + + return res.Networks, err +} + +type NetworkResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any Network resource +// response as a Network struct. +func (r NetworkResult) Extract() (*Network, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *Network `json:"network" mapstructure:"network"` + } + + err := mapstructure.Decode(r.Body, &res) + return res.Network, err +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a Network. +type GetResult struct { + NetworkResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/urls.go new file mode 100644 index 000000000000..2401a5d038f7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/urls.go @@ -0,0 +1,17 @@ +package tenantnetworks + +import "github.com/rackspace/gophercloud" + +const resourcePath = "os-tenant-networks" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/urls_test.go new file mode 100644 index 000000000000..39c464e9fbb0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks/urls_test.go @@ -0,0 +1,25 @@ +package tenantnetworks + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + + th.CheckEquals(t, c.Endpoint+"os-tenant-networks", listURL(c)) +} + +func TestGetURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + id := "1" + + th.CheckEquals(t, c.Endpoint+"os-tenant-networks/"+id, getURL(c, id)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/doc.go new file mode 100644 index 000000000000..22f68d80e529 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/doc.go @@ -0,0 +1,3 @@ +// Package volumeattach provides the ability to attach and detach volumes +// to instances +package volumeattach diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/fixtures.go new file mode 100644 index 000000000000..a7f03b322c89 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/fixtures.go @@ -0,0 +1,138 @@ +// +build fixtures + +package volumeattach + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput is a sample response to a List call. +const ListOutput = ` +{ + "volumeAttachments": [ + { + "device": "/dev/vdd", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f803", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803" + }, + { + "device": "/dev/vdc", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804" + } + ] +} +` + +// GetOutput is a sample response to a Get call. +const GetOutput = ` +{ + "volumeAttachment": { + "device": "/dev/vdc", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804" + } +} +` + +// CreateOutput is a sample response to a Create call. +const CreateOutput = ` +{ + "volumeAttachment": { + "device": "/dev/vdc", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804" + } +} +` + +// FirstVolumeAttachment is the first result in ListOutput. +var FirstVolumeAttachment = VolumeAttachment{ + Device: "/dev/vdd", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803", +} + +// SecondVolumeAttachment is the first result in ListOutput. +var SecondVolumeAttachment = VolumeAttachment{ + Device: "/dev/vdc", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", +} + +// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed +// from ListOutput, in the expected order. +var ExpectedVolumeAttachmentSlice = []VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment} + +// CreatedVolumeAttachment is the parsed result from CreatedOutput. +var CreatedVolumeAttachment = VolumeAttachment{ + Device: "/dev/vdc", + ID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", +} + +// HandleListSuccessfully configures the test server to respond to a List request. +func HandleListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for an existing attachment +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments/a26887c6-c47b-4654-abb5-dfadf7d3f804", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} + +// HandleCreateSuccessfully configures the test server to respond to a Create request +// for a new attachment +func HandleCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` +{ + "volumeAttachment": { + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "device": "/dev/vdc" + } +} +`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, CreateOutput) + }) +} + +// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a +// an existing attachment +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments/a26887c6-c47b-4654-abb5-dfadf7d3f804", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/requests.go new file mode 100644 index 000000000000..b4ebedea86ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/requests.go @@ -0,0 +1,75 @@ +package volumeattach + +import ( + "errors" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of VolumeAttachments. +func List(client *gophercloud.ServiceClient, serverId string) pagination.Pager { + return pagination.NewPager(client, listURL(client, serverId), func(r pagination.PageResult) pagination.Page { + return VolumeAttachmentsPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToVolumeAttachmentCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies volume attachment creation or import parameters. +type CreateOpts struct { + // Device is the device that the volume will attach to the instance as. Omit for "auto" + Device string + + // VolumeID is the ID of the volume to attach to the instance + VolumeID string +} + +// ToVolumeAttachmentCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToVolumeAttachmentCreateMap() (map[string]interface{}, error) { + if opts.VolumeID == "" { + return nil, errors.New("Missing field required for volume attachment creation: VolumeID") + } + + volumeAttachment := make(map[string]interface{}) + volumeAttachment["volumeId"] = opts.VolumeID + if opts.Device != "" { + volumeAttachment["device"] = opts.Device + } + + return map[string]interface{}{"volumeAttachment": volumeAttachment}, nil +} + +// Create requests the creation of a new volume attachment on the server +func Create(client *gophercloud.ServiceClient, serverId string, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToVolumeAttachmentCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(createURL(client, serverId), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Get returns public data about a previously created VolumeAttachment. +func Get(client *gophercloud.ServiceClient, serverId, aId string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, serverId, aId), &res.Body, nil) + return res +} + +// Delete requests the deletion of a previous stored VolumeAttachment from the server. +func Delete(client *gophercloud.ServiceClient, serverId, aId string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(deleteURL(client, serverId, aId), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/requests_test.go new file mode 100644 index 000000000000..e17f7e00637d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/requests_test.go @@ -0,0 +1,65 @@ +package volumeattach + +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() + HandleListSuccessfully(t) + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + count := 0 + err := List(client.ServiceClient(), serverId).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVolumeAttachments(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedVolumeAttachmentSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t) + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + actual, err := Create(client.ServiceClient(), serverId, CreateOpts{ + Device: "/dev/vdc", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &CreatedVolumeAttachment, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t) + aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + actual, err := Get(client.ServiceClient(), serverId, aId).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecondVolumeAttachment, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + err := Delete(client.ServiceClient(), serverId, aId).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/results.go new file mode 100644 index 000000000000..26be39e4f706 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/results.go @@ -0,0 +1,84 @@ +package volumeattach + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// VolumeAttach controls the attachment of a volume to an instance. +type VolumeAttachment struct { + // ID is a unique id of the attachment + ID string `mapstructure:"id"` + + // Device is what device the volume is attached as + Device string `mapstructure:"device"` + + // VolumeID is the ID of the attached volume + VolumeID string `mapstructure:"volumeId"` + + // ServerID is the ID of the instance that has the volume attached + ServerID string `mapstructure:"serverId"` +} + +// VolumeAttachmentsPage stores a single, only page of VolumeAttachments +// results from a List call. +type VolumeAttachmentsPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a VolumeAttachmentsPage is empty. +func (page VolumeAttachmentsPage) IsEmpty() (bool, error) { + va, err := ExtractVolumeAttachments(page) + return len(va) == 0, err +} + +// ExtractVolumeAttachments interprets a page of results as a slice of +// VolumeAttachments. +func ExtractVolumeAttachments(page pagination.Page) ([]VolumeAttachment, error) { + casted := page.(VolumeAttachmentsPage).Body + var response struct { + VolumeAttachments []VolumeAttachment `mapstructure:"volumeAttachments"` + } + + err := mapstructure.WeakDecode(casted, &response) + + return response.VolumeAttachments, err +} + +type VolumeAttachmentResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any VolumeAttachment resource +// response as a VolumeAttachment struct. +func (r VolumeAttachmentResult) Extract() (*VolumeAttachment, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VolumeAttachment *VolumeAttachment `json:"volumeAttachment" mapstructure:"volumeAttachment"` + } + + err := mapstructure.Decode(r.Body, &res) + return res.VolumeAttachment, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a VolumeAttachment. +type CreateResult struct { + VolumeAttachmentResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a VolumeAttachment. +type GetResult struct { + VolumeAttachmentResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/urls.go new file mode 100644 index 000000000000..9d9d1786db3e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/urls.go @@ -0,0 +1,25 @@ +package volumeattach + +import "github.com/rackspace/gophercloud" + +const resourcePath = "os-volume_attachments" + +func resourceURL(c *gophercloud.ServiceClient, serverId string) string { + return c.ServiceURL("servers", serverId, resourcePath) +} + +func listURL(c *gophercloud.ServiceClient, serverId string) string { + return resourceURL(c, serverId) +} + +func createURL(c *gophercloud.ServiceClient, serverId string) string { + return resourceURL(c, serverId) +} + +func getURL(c *gophercloud.ServiceClient, serverId, aId string) string { + return c.ServiceURL("servers", serverId, resourcePath, aId) +} + +func deleteURL(c *gophercloud.ServiceClient, serverId, aId string) string { + return getURL(c, serverId, aId) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/urls_test.go new file mode 100644 index 000000000000..8ee0e42d4564 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/urls_test.go @@ -0,0 +1,46 @@ +package volumeattach + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/os-volume_attachments", listURL(c, serverId)) +} + +func TestCreateURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/os-volume_attachments", createURL(c, serverId)) +} + +func TestGetURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" + + th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/os-volume_attachments/"+aId, getURL(c, serverId, aId)) +} + +func TestDeleteURL(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + c := client.ServiceClient() + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" + + th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/os-volume_attachments/"+aId, deleteURL(c, serverId, aId)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/doc.go new file mode 100644 index 000000000000..5822e1bcf6d2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/doc.go @@ -0,0 +1,7 @@ +// Package flavors provides information and interaction with the flavor API +// resource in the OpenStack Compute service. +// +// A flavor is an available hardware configuration for a server. Each flavor +// has a unique combination of disk space, memory capacity and priority for CPU +// time. +package flavors diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go new file mode 100644 index 000000000000..586be67ac258 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests.go @@ -0,0 +1,68 @@ +package flavors + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +// ListOpts helps control the results returned by the List() function. +// For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20. +// Typically, software will use the last ID of the previous call to List to set the Marker for the current call. +type ListOpts struct { + + // ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided. + ChangesSince string `q:"changes-since"` + + // MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // Marker and Limit control paging. + // Marker instructs List where to start listing from. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of flavors. + Limit int `q:"limit"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListDetail instructs OpenStack to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier processing. +// See ListOpts for more details. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + createPage := func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPage) +} + +// Get instructs OpenStack to provide details on a single flavor, identified by its ID. +// Use ExtractFlavor to convert its result into a Flavor. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests_test.go new file mode 100644 index 000000000000..fbd7c3314022 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/requests_test.go @@ -0,0 +1,129 @@ +package flavors + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +const tokenID = "blerb" + +func TestListFlavors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "flavors": [ + { + "id": "1", + "name": "m1.tiny", + "disk": 1, + "ram": 512, + "vcpus": 1 + }, + { + "id": "2", + "name": "m2.small", + "disk": 10, + "ram": 1024, + "vcpus": 2 + } + ], + "flavors_links": [ + { + "href": "%s/flavors/detail?marker=2", + "rel": "next" + } + ] + } + `, th.Server.URL) + case "2": + fmt.Fprintf(w, `{ "flavors": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + pages := 0 + err := ListDetail(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := ExtractFlavors(page) + if err != nil { + return false, err + } + + expected := []Flavor{ + Flavor{ID: "1", Name: "m1.tiny", Disk: 1, RAM: 512, VCPUs: 1}, + Flavor{ID: "2", Name: "m2.small", Disk: 10, RAM: 1024, VCPUs: 2}, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Fatal(err) + } + if pages != 1 { + t.Errorf("Expected one page, got %d", pages) + } +} + +func TestGetFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "flavor": { + "id": "1", + "name": "m1.tiny", + "disk": 1, + "ram": 512, + "vcpus": 1, + "rxtx_factor": 1 + } + } + `) + }) + + actual, err := Get(fake.ServiceClient(), "12345").Extract() + if err != nil { + t.Fatalf("Unable to get flavor: %v", err) + } + + expected := &Flavor{ + ID: "1", + Name: "m1.tiny", + Disk: 1, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1, + } + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/results.go new file mode 100644 index 000000000000..8dddd705c93d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/results.go @@ -0,0 +1,122 @@ +package flavors + +import ( + "errors" + "reflect" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ErrCannotInterpret is returned by an Extract call if the response body doesn't have the expected structure. +var ErrCannotInterpet = errors.New("Unable to interpret a response body.") + +// GetResult temporarily holds the response from a Get call. +type GetResult struct { + gophercloud.Result +} + +// Extract provides access to the individual Flavor returned by the Get function. +func (gr GetResult) Extract() (*Flavor, error) { + if gr.Err != nil { + return nil, gr.Err + } + + var result struct { + Flavor Flavor `mapstructure:"flavor"` + } + + cfg := &mapstructure.DecoderConfig{ + DecodeHook: defaulter, + Result: &result, + } + decoder, err := mapstructure.NewDecoder(cfg) + if err != nil { + return nil, err + } + err = decoder.Decode(gr.Body) + return &result.Flavor, err +} + +// Flavor records represent (virtual) hardware configurations for server resources in a region. +type Flavor struct { + // The Id field contains the flavor's unique identifier. + // For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance. + ID string `mapstructure:"id"` + + // The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively. + Disk int `mapstructure:"disk"` + RAM int `mapstructure:"ram"` + + // The Name field provides a human-readable moniker for the flavor. + Name string `mapstructure:"name"` + + RxTxFactor float64 `mapstructure:"rxtx_factor"` + + // Swap indicates how much space is reserved for swap. + // If not provided, this field will be set to 0. + Swap int `mapstructure:"swap"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `mapstructure:"vcpus"` +} + +// FlavorPage contains a single page of the response from a List call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a page contains any results. +func (p FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(p) + if err != nil { + return true, err + } + return len(flavors) == 0, nil +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (p FlavorPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"flavors_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) { + if (from == reflect.String) && (to == reflect.Int) { + return 0, nil + } + return v, nil +} + +// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation. +func ExtractFlavors(page pagination.Page) ([]Flavor, error) { + casted := page.(FlavorPage).Body + var container struct { + Flavors []Flavor `mapstructure:"flavors"` + } + + cfg := &mapstructure.DecoderConfig{ + DecodeHook: defaulter, + Result: &container, + } + decoder, err := mapstructure.NewDecoder(cfg) + if err != nil { + return container.Flavors, err + } + err = decoder.Decode(casted) + if err != nil { + return container.Flavors, err + } + + return container.Flavors, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls.go new file mode 100644 index 000000000000..683c107dcbe9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls.go @@ -0,0 +1,13 @@ +package flavors + +import ( + "github.com/rackspace/gophercloud" +) + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls_test.go new file mode 100644 index 000000000000..069da2496e05 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/flavors/urls_test.go @@ -0,0 +1,26 @@ +package flavors + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "flavors/foo" + th.CheckEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "flavors/detail" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/doc.go new file mode 100644 index 000000000000..0edaa3f02556 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/doc.go @@ -0,0 +1,7 @@ +// Package images provides information and interaction with the image API +// resource in the OpenStack Compute service. +// +// An image is a collection of files used to create or rebuild a server. +// Operators provide a number of pre-built OS images by default. You may also +// create custom images from cloud servers you have launched. +package images diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go new file mode 100644 index 000000000000..7ce5139519c7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests.go @@ -0,0 +1,72 @@ +package images + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts contain options for limiting the number of Images returned from a call to ListDetail. +type ListOpts struct { + // When the image last changed status (in date-time format). + ChangesSince string `q:"changes-since"` + // The number of Images to return. + Limit int `q:"limit"` + // UUID of the Image at which to set a marker. + Marker string `q:"marker"` + // The name of the Image. + Name string `q:"name"` + // The name of the Server (in URL format). + Server string `q:"server"` + // The current status of the Image. + Status string `q:"status"` + // The value of the type of image (e.g. BASE, SERVER, ALL) + Type string `q:"type"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListDetail enumerates the available images. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPage := func(r pagination.PageResult) pagination.Page { + return ImagePage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPage) +} + +// Get acquires additional detail about a specific image by ID. +// Use ExtractImage() to interpret the result as an openstack Image. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + _, result.Err = client.Get(getURL(client, id), &result.Body, nil) + return result +} + +// Delete deletes the specified image ID. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var result DeleteResult + _, result.Err = client.Delete(deleteURL(client, id), nil) + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests_test.go new file mode 100644 index 000000000000..93a97bdc65b5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/requests_test.go @@ -0,0 +1,191 @@ +package images + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListImages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ` + { + "images": [ + { + "status": "ACTIVE", + "updated": "2014-09-23T12:54:56Z", + "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + "OS-EXT-IMG-SIZE:size": 476704768, + "name": "F17-x86_64-cfntools", + "created": "2014-09-23T12:54:52Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-23T12:51:43Z", + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "OS-EXT-IMG-SIZE:size": 13167616, + "name": "cirros-0.3.2-x86_64-disk", + "created": "2014-09-23T12:51:42Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": {} + } + ] + } + `) + case "2": + fmt.Fprintf(w, `{ "images": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + pages := 0 + options := &ListOpts{Limit: 2} + err := ListDetail(fake.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := ExtractImages(page) + if err != nil { + return false, err + } + + expected := []Image{ + Image{ + ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + Name: "F17-x86_64-cfntools", + Created: "2014-09-23T12:54:52Z", + Updated: "2014-09-23T12:54:56Z", + MinDisk: 0, + MinRAM: 0, + Progress: 100, + Status: "ACTIVE", + }, + Image{ + ID: "f90f6034-2570-4974-8351-6b49732ef2eb", + Name: "cirros-0.3.2-x86_64-disk", + Created: "2014-09-23T12:51:42Z", + Updated: "2014-09-23T12:51:43Z", + MinDisk: 0, + MinRAM: 0, + Progress: 100, + Status: "ACTIVE", + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Unexpected page contents: expected %#v, got %#v", expected, actual) + } + + return false, nil + }) + + if err != nil { + t.Fatalf("EachPage error: %v", err) + } + if pages != 1 { + t.Errorf("Expected one page, got %d", pages) + } +} + +func TestGetImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "image": { + "status": "ACTIVE", + "updated": "2014-09-23T12:54:56Z", + "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + "OS-EXT-IMG-SIZE:size": 476704768, + "name": "F17-x86_64-cfntools", + "created": "2014-09-23T12:54:52Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": {} + } + } + `) + }) + + actual, err := Get(fake.ServiceClient(), "12345678").Extract() + if err != nil { + t.Fatalf("Unexpected error from Get: %v", err) + } + + expected := &Image{ + Status: "ACTIVE", + Updated: "2014-09-23T12:54:56Z", + ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7", + Name: "F17-x86_64-cfntools", + Created: "2014-09-23T12:54:52Z", + MinDisk: 0, + Progress: 100, + MinRAM: 0, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but got %#v", expected, actual) + } +} + +func TestNextPageURL(t *testing.T) { + var page ImagePage + var body map[string]interface{} + bodyString := []byte(`{"images":{"links":[{"href":"http://192.154.23.87/12345/images/image3","rel":"bookmark"}]}, "images_links":[{"href":"http://192.154.23.87/12345/images/image4","rel":"next"}]}`) + err := json.Unmarshal(bodyString, &body) + if err != nil { + t.Fatalf("Error unmarshaling data into page body: %v", err) + } + page.Body = body + + expected := "http://192.154.23.87/12345/images/image4" + actual, err := page.NextPageURL() + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) +} + +// Test Image delete +func TestDeleteImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "12345678") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go new file mode 100644 index 000000000000..40e814d1de0c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/results.go @@ -0,0 +1,95 @@ +package images + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// GetResult temporarily stores a Get response. +type GetResult struct { + gophercloud.Result +} + +// DeleteResult represents the result of an image.Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Extract interprets a GetResult as an Image. +func (gr GetResult) Extract() (*Image, error) { + if gr.Err != nil { + return nil, gr.Err + } + + var decoded struct { + Image Image `mapstructure:"image"` + } + + err := mapstructure.Decode(gr.Body, &decoded) + return &decoded.Image, err +} + +// Image is used for JSON (un)marshalling. +// It provides a description of an OS image. +type Image struct { + // ID contains the image's unique identifier. + ID string + + Created string + + // MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image. + MinDisk int + MinRAM int + + // Name provides a human-readable moniker for the OS image. + Name string + + // The Progress and Status fields indicate image-creation status. + // Any usable image will have 100% progress. + Progress int + Status string + + Updated string +} + +// ImagePage contains a single page of results from a List operation. +// Use ExtractImages to convert it into a slice of usable structs. +type ImagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Image results. +func (page ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(page) + if err != nil { + return true, err + } + return len(images) == 0, nil +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page ImagePage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"images_links"` + } + + var r resp + err := mapstructure.Decode(page.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// ExtractImages converts a page of List results into a slice of usable Image structs. +func ExtractImages(page pagination.Page) ([]Image, error) { + casted := page.(ImagePage).Body + var results struct { + Images []Image `mapstructure:"images"` + } + + err := mapstructure.Decode(casted, &results) + return results.Images, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls.go new file mode 100644 index 000000000000..b1bf1038fb08 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls.go @@ -0,0 +1,15 @@ +package images + +import "github.com/rackspace/gophercloud" + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("images", "detail") +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls_test.go new file mode 100644 index 000000000000..b1ab3d6790cd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/images/urls_test.go @@ -0,0 +1,26 @@ +package images + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "images/foo" + th.CheckEquals(t, expected, actual) +} + +func TestListDetailURL(t *testing.T) { + actual := listDetailURL(endpointClient()) + expected := endpoint + "images/detail" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/doc.go new file mode 100644 index 000000000000..fe4567120c7d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/doc.go @@ -0,0 +1,6 @@ +// Package servers provides information and interaction with the server API +// resource in the OpenStack Compute service. +// +// A server is a virtual machine instance in the compute system. In order for +// one to be provisioned, a valid flavor and image are required. +package servers 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 new file mode 100644 index 000000000000..4339a16d4404 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/fixtures.go @@ -0,0 +1,664 @@ +// +build fixtures + +package servers + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ServerListBody contains the canned body of a servers.List response. +const ServerListBody = ` +{ + "servers": [ + { + "status": "ACTIVE", + "updated": "2014-09-25T13:10:10Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": 4, + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001e", + "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "herp", + "created": "2014-09-25T13:10:02Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } + ] +} +` + +// SingleServerBody is the canned body of a Get request on an existing server. +const SingleServerBody = ` +{ + "server": { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } +} +` + +var ( + // ServerHerp is a Server struct that should correspond to the first result in ServerListBody. + ServerHerp = Server{ + Status: "ACTIVE", + Updated: "2014-09-25T13:10:10Z", + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": float64(4), + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark", + }, + }, + Image: map[string]interface{}{ + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "herp", + Created: "2014-09-25T13:10:02Z", + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]interface{}{}, + SecurityGroups: []map[string]interface{}{ + map[string]interface{}{ + "name": "default", + }, + }, + } + + // ServerDerp is a Server struct that should correspond to the second server in ServerListBody. + ServerDerp = Server{ + Status: "ACTIVE", + Updated: "2014-09-25T13:04:49Z", + HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": float64(4), + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed", + }, + }, + }, + Links: []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self", + }, + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark", + }, + }, + Image: map[string]interface{}{ + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "1", + "links": []interface{}{ + map[string]interface{}{ + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark", + }, + }, + }, + ID: "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + UserID: "9349aff8be7545ac9d2f1d00999a23cd", + Name: "derp", + Created: "2014-09-25T13:04:41Z", + TenantID: "fcad67a6189847c4aecfa3c81a05783b", + Metadata: map[string]interface{}{}, + SecurityGroups: []map[string]interface{}{ + map[string]interface{}{ + "name": "default", + }, + }, + } +) + +// HandleServerCreationSuccessfully sets up the test server to respond to a server creation request +// with a given response. +func HandleServerCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "server": { + "name": "derp", + "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb", + "flavorRef": "1" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerListSuccessfully sets up the test server to respond to a server List request. +func HandleServerListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ServerListBody) + case "9e5476bd-a4ec-4653-93d6-72c93aa682ba": + fmt.Fprintf(w, `{ "servers": [] }`) + default: + t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandleServerDeletionSuccessfully sets up the test server to respond to a server deletion request. +func HandleServerDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleServerGetSuccessfully sets up the test server to respond to a server Get request. +func HandleServerGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SingleServerBody) + }) +} + +// HandleServerUpdateSuccessfully sets up the test server to respond to a server Update request. +func HandleServerUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`) + + fmt.Fprintf(w, SingleServerBody) + }) +} + +// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password +// change request. +func HandleAdminPasswordChangeSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleRebootSuccessfully sets up the test server to respond to a reboot request with success. +func HandleRebootSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`) + + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleRebuildSuccessfully sets up the test server to respond to a rebuild request with success. +func HandleRebuildSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ` + { + "rebuild": { + "name": "new-name", + "adminPass": "swordfish", + "imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "accessIPv4": "1.2.3.4" + } + } + `) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandleServerRescueSuccessfully sets up the test server to respond to a server Rescue request. +func HandleServerRescueSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "rescue": { "adminPass": "1234567890" } }`) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ "adminPass": "1234567890" }`)) + }) +} + +// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request. +func HandleMetadatumGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "meta": {"foo":"bar"}}`)) + }) +} + +// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request. +func HandleMetadatumCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "meta": { + "foo": "bar" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "meta": {"foo":"bar"}}`)) + }) +} + +// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request. +func HandleMetadatumDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request. +func HandleMetadataGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`)) + }) +} + +// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request. +func HandleMetadataResetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "metadata": { + "foo": "bar", + "this": "that" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`)) + }) +} + +// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request. +func HandleMetadataUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "metadata": { + "foo": "baz", + "this": "those" + } + }`) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(`{ "metadata": {"foo":"baz", "this":"those"}}`)) + }) +} + +// ListAddressesExpected represents an expected repsonse from a ListAddresses request. +var ListAddressesExpected = map[string][]Address{ + "public": []Address{ + Address{ + Version: 4, + Address: "80.56.136.39", + }, + Address{ + Version: 6, + Address: "2001:4800:790e:510:be76:4eff:fe04:82a8", + }, + }, + "private": []Address{ + Address{ + Version: 4, + Address: "10.880.3.154", + }, + }, +} + +// HandleAddressListSuccessfully sets up the test server to respond to a ListAddresses request. +func HandleAddressListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf/ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "addresses": { + "public": [ + { + "version": 4, + "addr": "50.56.176.35" + }, + { + "version": 6, + "addr": "2001:4800:780e:510:be76:4eff:fe04:84a8" + } + ], + "private": [ + { + "version": 4, + "addr": "10.180.3.155" + } + ] + } + }`) + }) +} + +// ListNetworkAddressesExpected represents an expected repsonse from a ListAddressesByNetwork request. +var ListNetworkAddressesExpected = []Address{ + Address{ + Version: 4, + Address: "50.56.176.35", + }, + Address{ + Version: 6, + Address: "2001:4800:780e:510:be76:4eff:fe04:84a8", + }, +} + +// HandleNetworkAddressListSuccessfully sets up the test server to respond to a ListAddressesByNetwork request. +func HandleNetworkAddressListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/asdfasdfasdf/ips/public", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "public": [ + { + "version": 4, + "addr": "50.56.176.35" + }, + { + "version": 6, + "addr": "2001:4800:780e:510:be76:4eff:fe04:84a8" + } + ] + }`) + }) +} + +// HandleCreateServerImageSuccessfully sets up the test server to respond to a TestCreateServerImage request. +func HandleCreateServerImageSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/servers/serverimage/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.Header().Add("Location", "https://0.0.0.0/images/xxxx-xxxxx-xxxxx-xxxx") + w.WriteHeader(http.StatusAccepted) + }) +} + 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 new file mode 100644 index 000000000000..aa8c1a87b277 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests.go @@ -0,0 +1,745 @@ +package servers + +import ( + "encoding/base64" + "errors" + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // A time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Name of the image in URL format. + Image string `q:"image"` + + // Name of the flavor in URL format. + Flavor string `q:"flavor"` + + // Name of the server as a string; can be queried with regular expressions. + // Realize that ?name=bob returns both bob and bobb. If you need to match bob + // only, you can use a regular expression matching the syntax of the + // underlying database server implemented for Compute. + Name string `q:"name"` + + // Value of the status of the server so that you can filter on "ACTIVE" for example. + Status string `q:"status"` + + // Name of the host as a string. + Host string `q:"host"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Integer value for the limit of values to return. + Limit int `q:"limit"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List makes a request against the API to list servers accessible to you. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPageFn := func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPageFn) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +// The CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// Network is used within CreateOpts to control a new server's network attachments. +type Network struct { + // UUID of a nova-network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP [optional] specifies a fixed IPv4 address to be used on this network. + FixedIP string +} + +// CreateOpts specifies server creation parameters. +type CreateOpts struct { + // Name [required] is the name to assign to the newly launched server. + Name string + + // ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state. + // Optional if using the boot-from-volume extension. + ImageRef string + + // FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs. + FlavorRef string + + // SecurityGroups [optional] lists the names of the security groups to which this server should belong. + SecurityGroups []string + + // UserData [optional] contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you. + UserData []byte + + // AvailabilityZone [optional] in which to launch the server. + AvailabilityZone string + + // Networks [optional] dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the tenant. + Networks []Network + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string + + // Personality [optional] includes the path and contents of a file to inject into the server at launch. + // The maximum size of the file is 255 bytes (decoded). + Personality []byte + + // ConfigDrive [optional] enables metadata injection through a configuration drive. + ConfigDrive bool + + // 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. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + server := make(map[string]interface{}) + + server["name"] = opts.Name + server["imageRef"] = opts.ImageRef + server["flavorRef"] = opts.FlavorRef + + if opts.UserData != nil { + encoded := base64.StdEncoding.EncodeToString(opts.UserData) + server["user_data"] = &encoded + } + if opts.Personality != nil { + encoded := base64.StdEncoding.EncodeToString(opts.Personality) + server["personality"] = &encoded + } + if opts.ConfigDrive { + server["config_drive"] = "true" + } + if opts.AvailabilityZone != "" { + server["availability_zone"] = opts.AvailabilityZone + } + if opts.Metadata != nil { + server["metadata"] = opts.Metadata + } + if 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 { + networks := make([]map[string]interface{}, len(opts.Networks)) + for i, net := range opts.Networks { + networks[i] = make(map[string]interface{}) + if net.UUID != "" { + networks[i]["uuid"] = net.UUID + } + if net.Port != "" { + networks[i]["port"] = net.Port + } + if net.FixedIP != "" { + networks[i]["fixed_ip"] = net.FixedIP + } + } + server["networks"] = networks + } + + return map[string]interface{}{"server": server}, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToServerCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(listURL(client), reqBody, &res.Body, nil) + return res +} + +// Delete requests that a server previously provisioned be removed from your account. +func Delete(client *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(deleteURL(client, id), nil) + return res +} + +// Get requests details on a single server, by ID. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var result GetResult + _, result.Err = client.Get(getURL(client, id), &result.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + return result +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() map[string]interface{} +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts struct { + // Name [optional] changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name string + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() map[string]interface{} { + server := make(map[string]string) + if opts.Name != "" { + server["name"] = opts.Name + } + if opts.AccessIPv4 != "" { + server["accessIPv4"] = opts.AccessIPv4 + } + if opts.AccessIPv6 != "" { + server["accessIPv6"] = opts.AccessIPv6 + } + return map[string]interface{}{"server": server} +} + +// Update requests that various attributes of the indicated server be changed. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var result UpdateResult + reqBody := opts.ToServerUpdateMap() + _, result.Err = client.Put(updateURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return result +} + +// ChangeAdminPassword alters the administrator or root password for a specified server. +func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) ActionResult { + var req struct { + ChangePassword struct { + AdminPass string `json:"adminPass"` + } `json:"changePassword"` + } + + req.ChangePassword.AdminPass = newPassword + + var res ActionResult + _, res.Err = client.Post(actionURL(client, id), req, nil, nil) + return res +} + +// ErrArgument errors occur when an argument supplied to a package function +// fails to fall within acceptable values. For example, the Reboot() function +// expects the "how" parameter to be one of HardReboot or SoftReboot. These +// constants are (currently) strings, leading someone to wonder if they can pass +// other string values instead, perhaps in an effort to break the API of their +// provider. Reboot() returns this error in this situation. +// +// Function identifies which function was called/which function is generating +// the error. +// Argument identifies which formal argument was responsible for producing the +// error. +// Value provides the value as it was passed into the function. +type ErrArgument struct { + Function, Argument string + Value interface{} +} + +// Error yields a useful diagnostic for debugging purposes. +func (e *ErrArgument) Error() string { + return fmt.Sprintf("Bad argument in call to %s, formal parameter %s, value %#v", e.Function, e.Argument, e.Value) +} + +func (e *ErrArgument) String() string { + return e.Error() +} + +// RebootMethod describes the mechanisms by which a server reboot can be requested. +type RebootMethod string + +// These constants determine how a server should be rebooted. +// See the Reboot() function for further details. +const ( + SoftReboot RebootMethod = "SOFT" + HardReboot RebootMethod = "HARD" + OSReboot = SoftReboot + PowerCycle = HardReboot +) + +// Reboot requests that a given server reboot. +// Two methods exist for rebooting a server: +// +// HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the machine, or if a VM, +// terminating it at the hypervisor level. +// It's done. Caput. Full stop. +// Then, after a brief while, power is restored or the VM instance restarted. +// +// SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures. +// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine. +func Reboot(client *gophercloud.ServiceClient, id string, how RebootMethod) ActionResult { + var res ActionResult + + if (how != SoftReboot) && (how != HardReboot) { + res.Err = &ErrArgument{ + Function: "Reboot", + Argument: "how", + Value: how, + } + return res + } + + reqBody := struct { + C map[string]string `json:"reboot"` + }{ + map[string]string{"type": string(how)}, + } + + _, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil) + return res +} + +// RebuildOptsBuilder is an interface that allows extensions to override the +// default behaviour of rebuild options +type RebuildOptsBuilder interface { + ToServerRebuildMap() (map[string]interface{}, error) +} + +// RebuildOpts represents the configuration options used in a server rebuild +// operation +type RebuildOpts struct { + // Required. The ID of the image you want your server to be provisioned on + ImageID string + + // Name to set the server to + Name string + + // Required. The server's admin password + AdminPass string + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string + + // Personality [optional] includes the path and contents of a file to inject into the server at launch. + // The maximum size of the file is 255 bytes (decoded). + Personality []byte +} + +// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON +func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { + var err error + server := make(map[string]interface{}) + + if opts.AdminPass == "" { + err = fmt.Errorf("AdminPass is required") + } + + if opts.ImageID == "" { + err = fmt.Errorf("ImageID is required") + } + + if err != nil { + return server, err + } + + server["name"] = opts.Name + server["adminPass"] = opts.AdminPass + server["imageRef"] = opts.ImageID + + if opts.AccessIPv4 != "" { + server["accessIPv4"] = opts.AccessIPv4 + } + + if opts.AccessIPv6 != "" { + server["accessIPv6"] = opts.AccessIPv6 + } + + if opts.Metadata != nil { + server["metadata"] = opts.Metadata + } + + if opts.Personality != nil { + encoded := base64.StdEncoding.EncodeToString(opts.Personality) + server["personality"] = &encoded + } + + return map[string]interface{}{"rebuild": server}, nil +} + +// Rebuild will reprovision the server according to the configuration options +// provided in the RebuildOpts struct. +func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) RebuildResult { + var result RebuildResult + + if id == "" { + result.Err = fmt.Errorf("ID is required") + return result + } + + reqBody, err := opts.ToServerRebuildMap() + if err != nil { + result.Err = err + return result + } + + _, result.Err = client.Post(actionURL(client, id), reqBody, &result.Body, nil) + return result +} + +// ResizeOptsBuilder is an interface that allows extensions to override the default structure of +// a Resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]interface{}, error) +} + +// ResizeOpts represents the configuration options used to control a Resize operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string +} + +// 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{}{ + "flavorRef": opts.FlavorRef, + } + + return map[string]interface{}{"resize": resize}, nil +} + +// Resize instructs the provider to change the flavor of the server. +// Note that this implies rebuilding it. +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in RESIZE_VERIFY state. +// While in this state, you can explore the use of the new server's configuration. +// If you like it, call ConfirmResize() to commit the resize permanently. +// Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) ActionResult { + var res ActionResult + reqBody, err := opts.ToServerResizeMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil) + return res +} + +// ConfirmResize confirms a previous resize operation on a server. +// See Resize() for more details. +func ConfirmResize(client *gophercloud.ServiceClient, id string) ActionResult { + var res ActionResult + + reqBody := map[string]interface{}{"confirmResize": nil} + _, res.Err = client.Post(actionURL(client, id), reqBody, nil, &gophercloud.RequestOpts{ + OkCodes: []int{201, 202, 204}, + }) + return res +} + +// RevertResize cancels a previous resize operation on a server. +// See Resize() for more details. +func RevertResize(client *gophercloud.ServiceClient, id string) ActionResult { + var res ActionResult + reqBody := map[string]interface{}{"revertResize": nil} + _, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil) + 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 = client.Post(actionURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{ + 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 = client.Put(metadataURL(client, id), metadata, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + 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 = client.Get(metadataURL(client, id), &res.Body, nil) + 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 = client.Post(metadataURL(client, id), metadata, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + 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 = client.Put(metadatumURL(client, id, key), metadatum, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + 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 = client.Request("GET", metadatumURL(client, id, key), gophercloud.RequestOpts{ + JSONResponse: &res.Body, + }) + 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 = client.Delete(metadatumURL(client, id, key), &gophercloud.RequestOpts{ + JSONResponse: &res.Body, + }) + return res +} + +// ListAddresses makes a request against the API to list the servers IP addresses. +func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager { + createPageFn := func(r pagination.PageResult) pagination.Page { + return AddressPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, listAddressesURL(client, id), createPageFn) +} + +// ListAddressesByNetwork makes a request against the API to list the servers IP addresses +// for the given network. +func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager { + createPageFn := func(r pagination.PageResult) pagination.Page { + return NetworkAddressPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), createPageFn) +} + +type CreateImageOpts struct { + // Name [required] of the image/snapshot + Name string + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the created image. + Metadata map[string]string +} + +type CreateImageOptsBuilder interface { + ToServerCreateImageMap() (map[string]interface{}, error) +} + +// ToServerCreateImageMap formats a CreateImageOpts structure into a request body. +func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) { + var err error + img := make(map[string]interface{}) + if opts.Name == "" { + return nil, fmt.Errorf("Cannot create a server image without a name") + } + img["name"] = opts.Name + if opts.Metadata != nil { + img["metadata"] = opts.Metadata + } + createImage := make(map[string]interface{}) + createImage["createImage"] = img + return createImage, err +} + +// CreateImage makes a request against the nova API to schedule an image to be created of the server +func CreateImage(client *gophercloud.ServiceClient, serverId string, opts CreateImageOptsBuilder) CreateImageResult { + var res CreateImageResult + reqBody, err := opts.ToServerCreateImageMap() + if err != nil { + res.Err = err + return res + } + response, err := client.Post(actionURL(client, serverId), reqBody, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + res.Err = err + res.Header = response.Header + 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 new file mode 100644 index 000000000000..1f39fe143bd0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/requests_test.go @@ -0,0 +1,336 @@ +package servers + +import ( + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerListSuccessfully(t) + + pages := 0 + err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := ExtractServers(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 servers, got %d", len(actual)) + } + th.CheckDeepEquals(t, ServerHerp, actual[0]) + th.CheckDeepEquals(t, ServerDerp, actual[1]) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListAllServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerListSuccessfully(t) + + allPages, err := List(client.ServiceClient(), ListOpts{}).AllPages() + th.AssertNoErr(t, err) + actual, err := ExtractServers(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ServerHerp, actual[0]) + th.CheckDeepEquals(t, ServerDerp, actual[1]) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerCreationSuccessfully(t, SingleServerBody) + + actual, err := Create(client.ServiceClient(), CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerDeletionSuccessfully(t) + + res := Delete(client.ServiceClient(), "asdfasdfasdf") + th.AssertNoErr(t, res.Err) +} + +func TestGetServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerGetSuccessfully(t) + + client := client.ServiceClient() + actual, err := Get(client, "1234asdf").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestUpdateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleServerUpdateSuccessfully(t) + + client := client.ServiceClient() + actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract() + if err != nil { + t.Fatalf("Unexpected Update error: %v", err) + } + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestChangeServerAdminPassword(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAdminPasswordChangeSuccessfully(t) + + res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password") + th.AssertNoErr(t, res.Err) +} + +func TestRebootServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRebootSuccessfully(t) + + res := Reboot(client.ServiceClient(), "1234asdf", SoftReboot) + th.AssertNoErr(t, res.Err) +} + +func TestRebuildServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleRebuildSuccessfully(t, SingleServerBody) + + opts := RebuildOpts{ + Name: "new-name", + AdminPass: "swordfish", + ImageID: "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + AccessIPv4: "1.2.3.4", + } + + actual, err := Rebuild(client.ServiceClient(), "1234asdf", opts).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ServerDerp, *actual) +} + +func TestResizeServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`) + + w.WriteHeader(http.StatusAccepted) + }) + + res := Resize(client.ServiceClient(), "1234asdf", ResizeOpts{FlavorRef: "2"}) + th.AssertNoErr(t, res.Err) +} + +func TestConfirmResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "confirmResize": null }`) + + w.WriteHeader(http.StatusNoContent) + }) + + res := ConfirmResize(client.ServiceClient(), "1234asdf") + th.AssertNoErr(t, res.Err) +} + +func TestRevertResize(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ "revertResize": null }`) + + w.WriteHeader(http.StatusAccepted) + }) + + res := 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) +} + +func TestListAddresses(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAddressListSuccessfully(t) + + expected := ListAddressesExpected + pages := 0 + err := ListAddresses(client.ServiceClient(), "asdfasdfasdf").EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := ExtractAddresses(page) + th.AssertNoErr(t, err) + + if len(actual) != 2 { + t.Fatalf("Expected 2 networks, got %d", len(actual)) + } + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, pages) +} + +func TestListAddressesByNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleNetworkAddressListSuccessfully(t) + + expected := ListNetworkAddressesExpected + pages := 0 + err := ListAddressesByNetwork(client.ServiceClient(), "asdfasdfasdf", "public").EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := ExtractNetworkAddresses(page) + th.AssertNoErr(t, err) + + if len(actual) != 2 { + t.Fatalf("Expected 2 addresses, got %d", len(actual)) + } + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, pages) +} + +func TestCreateServerImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateServerImageSuccessfully(t) + + _, err := CreateImage(client.ServiceClient(), "serverimage", CreateImageOpts{Name: "test"}).ExtractImageID() + th.AssertNoErr(t, err) +} 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 new file mode 100644 index 000000000000..f27870984ae2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/results.go @@ -0,0 +1,372 @@ +package servers + +import ( + "reflect" + "fmt" + "path" + "net/url" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type serverResult struct { + gophercloud.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Server Server `mapstructure:"server"` + } + + 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. +type CreateResult struct { + serverResult +} + +// GetResult temporarily contains the response from a Get call. +type GetResult struct { + serverResult +} + +// UpdateResult temporarily contains the response from an Update call. +type UpdateResult struct { + serverResult +} + +// DeleteResult temporarily contains the response from a Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// RebuildResult temporarily contains the response from a Rebuild call. +type RebuildResult struct { + serverResult +} + +// ActionResult represents the result of server action operations, like reboot +type ActionResult struct { + gophercloud.ErrResult +} + +// RescueResult represents the result of a server rescue operation +type RescueResult struct { + ActionResult +} + +// CreateImageResult represents the result of an image creation operation +type CreateImageResult struct { + gophercloud.Result +} + +// ExtractImageID gets the ID of the newly created server image from the header +func (res CreateImageResult) ExtractImageID() (string, error) { + if res.Err != nil { + return "", res.Err + } + // Get the image id from the header + u, err := url.ParseRequestURI(res.Header.Get("Location")) + if err != nil { + return "", fmt.Errorf("Failed to parse the image id: %s", err.Error()) + } + imageId := path.Base(u.Path) + if imageId == "." || imageId == "/" { + return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u) + } + return imageId, nil +} + +// Extract interprets any RescueResult as an AdminPass, if possible. +func (r RescueResult) Extract() (string, error) { + 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. + ID string + + // TenantID identifies the tenant owning this server resource. + TenantID string `mapstructure:"tenant_id"` + + // UserID uniquely identifies the user account owning the tenant. + UserID string `mapstructure:"user_id"` + + // Name contains the human-readable name for the server. + Name string + + // Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created. + Updated string + Created string + + HostID string + + // Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE. + Status string + + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int + + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration. + AccessIPv4, AccessIPv6 string + + // Image refers to a JSON object, which itself indicates the OS image used to deploy the server. + Image map[string]interface{} + + // Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server. + Flavor map[string]interface{} + + // Addresses includes a list of all IP addresses assigned to the server, keyed by pool. + Addresses map[string]interface{} + + // Metadata includes a list of all user-specified key-value pairs attached to the server. + Metadata map[string]interface{} + + // Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference. + Links []interface{} + + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name" mapstructure:"key_name"` + + // AdminPass will generally be empty (""). However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass" mapstructure:"adminPass"` + + // SecurityGroups includes the security groups that this instance has applied to it + SecurityGroups []map[string]interface{} `json:"security_groups" mapstructure:"security_groups"` +} + +// ServerPage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (page ServerPage) IsEmpty() (bool, error) { + servers, err := ExtractServers(page) + if err != nil { + return true, err + } + return len(servers) == 0, nil +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page ServerPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"servers_links"` + } + + var r resp + err := mapstructure.Decode(page.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities. +func ExtractServers(page pagination.Page) ([]Server, error) { + casted := page.(ServerPage).Body + + var response struct { + Servers []Server `mapstructure:"servers"` + } + + config := &mapstructure.DecoderConfig{ + DecodeHook: toMapFromString, + Result: &response, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return nil, err + } + + err = decoder.Decode(casted) + + 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 +} + +// Address represents an IP address. +type Address struct { + Version int `mapstructure:"version"` + Address string `mapstructure:"addr"` +} + +// AddressPage abstracts the raw results of making a ListAddresses() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned +// to the client, you may only safely access the data provided through the ExtractAddresses call. +type AddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if an AddressPage contains no networks. +func (r AddressPage) IsEmpty() (bool, error) { + addresses, err := ExtractAddresses(r) + if err != nil { + return true, err + } + return len(addresses) == 0, nil +} + +// ExtractAddresses interprets the results of a single page from a ListAddresses() call, +// producing a map of addresses. +func ExtractAddresses(page pagination.Page) (map[string][]Address, error) { + casted := page.(AddressPage).Body + + var response struct { + Addresses map[string][]Address `mapstructure:"addresses"` + } + + err := mapstructure.Decode(casted, &response) + if err != nil { + return nil, err + } + + return response.Addresses, err +} + +// NetworkAddressPage abstracts the raw results of making a ListAddressesByNetwork() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned +// to the client, you may only safely access the data provided through the ExtractAddresses call. +type NetworkAddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a NetworkAddressPage contains no addresses. +func (r NetworkAddressPage) IsEmpty() (bool, error) { + addresses, err := ExtractNetworkAddresses(r) + if err != nil { + return true, err + } + return len(addresses) == 0, nil +} + +// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call, +// producing a slice of addresses. +func ExtractNetworkAddresses(page pagination.Page) ([]Address, error) { + casted := page.(NetworkAddressPage).Body + + var response map[string][]Address + err := mapstructure.Decode(casted, &response) + if err != nil { + return nil, err + } + + var key string + for k := range response { + key = k + } + + return response[key], err +} 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 new file mode 100644 index 000000000000..8998354939a0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls.go @@ -0,0 +1,47 @@ +package servers + +import "github.com/rackspace/gophercloud" + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *gophercloud.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +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") +} + +func listAddressesURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "ips") +} + +func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string { + return client.ServiceURL("servers", id, "ips", network) +} 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 new file mode 100644 index 000000000000..17a1d287f250 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/urls_test.go @@ -0,0 +1,68 @@ +package servers + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "servers" + th.CheckEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "servers" + th.CheckEquals(t, expected, actual) +} + +func TestListDetailURL(t *testing.T) { + actual := listDetailURL(endpointClient()) + expected := endpoint + "servers/detail" + th.CheckEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "servers/foo" + th.CheckEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "servers/foo" + th.CheckEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "servers/foo" + th.CheckEquals(t, expected, actual) +} + +func TestActionURL(t *testing.T) { + actual := actionURL(endpointClient(), "foo") + expected := endpoint + "servers/foo/action" + th.CheckEquals(t, expected, actual) +} + +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.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util.go new file mode 100644 index 000000000000..e6baf74165bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/compute/v2/servers/util.go @@ -0,0 +1,20 @@ +package servers + +import "github.com/rackspace/gophercloud" + +// WaitForStatus will continually poll a server until it successfully transitions to a specified +// status. It will do this for at most the number of seconds specified. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location.go new file mode 100644 index 000000000000..29d02c43f929 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location.go @@ -0,0 +1,91 @@ +package openstack + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens" +) + +// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired +// during the v2 identity service. The specified EndpointOpts are used to identify a unique, +// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided +// criteria and when none do. The minimum that can be specified is a Type, but you will also often +// need to specify a Name and/or a Region depending on what's available on your OpenStack +// deployment. +func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. + var endpoints = make([]tokens2.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Region == "" || endpoint.Region == opts.Region { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // Report an error if the options were ambiguous. + if len(endpoints) > 1 { + return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints) + } + + // Extract the appropriate URL from the matching Endpoint. + for _, endpoint := range endpoints { + switch opts.Availability { + case gophercloud.AvailabilityPublic: + return gophercloud.NormalizeURL(endpoint.PublicURL), nil + case gophercloud.AvailabilityInternal: + return gophercloud.NormalizeURL(endpoint.InternalURL), nil + case gophercloud.AvailabilityAdmin: + return gophercloud.NormalizeURL(endpoint.AdminURL), nil + default: + return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability) + } + } + + // Report an error if there were no matching endpoints. + return "", gophercloud.ErrEndpointNotFound +} + +// V3EndpointURL discovers the endpoint URL for a specific service from a Catalog acquired +// during the v3 identity service. The specified EndpointOpts are used to identify a unique, +// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided +// criteria and when none do. The minimum that can be specified is a Type, but you will also often +// need to specify a Name and/or a Region depending on what's available on your OpenStack +// deployment. +func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Interface, + // Name if provided, and Region if provided. + var endpoints = make([]tokens3.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Availability != gophercloud.AvailabilityAdmin && + opts.Availability != gophercloud.AvailabilityPublic && + opts.Availability != gophercloud.AvailabilityInternal { + return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability) + } + if (opts.Availability == gophercloud.Availability(endpoint.Interface)) && + (opts.Region == "" || endpoint.Region == opts.Region) { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // Report an error if the options were ambiguous. + if len(endpoints) > 1 { + return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints) + } + + // Extract the URL from the matching Endpoint. + for _, endpoint := range endpoints { + return gophercloud.NormalizeURL(endpoint.URL), nil + } + + // Report an error if there were no matching endpoints. + return "", gophercloud.ErrEndpointNotFound +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location_test.go new file mode 100644 index 000000000000..8e65918abfe7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/endpoint_location_test.go @@ -0,0 +1,228 @@ +package openstack + +import ( + "strings" + "testing" + + "github.com/rackspace/gophercloud" + tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens" + th "github.com/rackspace/gophercloud/testhelper" +) + +// Service catalog fixtures take too much vertical space! +var catalog2 = tokens2.ServiceCatalog{ + Entries: []tokens2.CatalogEntry{ + tokens2.CatalogEntry{ + Type: "same", + Name: "same", + Endpoints: []tokens2.Endpoint{ + tokens2.Endpoint{ + Region: "same", + PublicURL: "https://public.correct.com/", + InternalURL: "https://internal.correct.com/", + AdminURL: "https://admin.correct.com/", + }, + tokens2.Endpoint{ + Region: "different", + PublicURL: "https://badregion.com/", + }, + }, + }, + tokens2.CatalogEntry{ + Type: "same", + Name: "different", + Endpoints: []tokens2.Endpoint{ + tokens2.Endpoint{ + Region: "same", + PublicURL: "https://badname.com/", + }, + tokens2.Endpoint{ + Region: "different", + PublicURL: "https://badname.com/+badregion", + }, + }, + }, + tokens2.CatalogEntry{ + Type: "different", + Name: "different", + Endpoints: []tokens2.Endpoint{ + tokens2.Endpoint{ + Region: "same", + PublicURL: "https://badtype.com/+badname", + }, + tokens2.Endpoint{ + Region: "different", + PublicURL: "https://badtype.com/+badregion+badname", + }, + }, + }, + }, +} + +func TestV2EndpointExact(t *testing.T) { + expectedURLs := map[gophercloud.Availability]string{ + gophercloud.AvailabilityPublic: "https://public.correct.com/", + gophercloud.AvailabilityAdmin: "https://admin.correct.com/", + gophercloud.AvailabilityInternal: "https://internal.correct.com/", + } + + for availability, expected := range expectedURLs { + actual, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: availability, + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) + } +} + +func TestV2EndpointNone(t *testing.T) { + _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "nope", + Availability: gophercloud.AvailabilityPublic, + }) + th.CheckEquals(t, gophercloud.ErrEndpointNotFound, err) +} + +func TestV2EndpointMultiple(t *testing.T) { + _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "same", + Region: "same", + Availability: gophercloud.AvailabilityPublic, + }) + if !strings.HasPrefix(err.Error(), "Discovered 2 matching endpoints:") { + t.Errorf("Received unexpected error: %v", err) + } +} + +func TestV2EndpointBadAvailability(t *testing.T) { + _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: "wat", + }) + th.CheckEquals(t, "Unexpected availability in endpoint query: wat", err.Error()) +} + +var catalog3 = tokens3.ServiceCatalog{ + Entries: []tokens3.CatalogEntry{ + tokens3.CatalogEntry{ + Type: "same", + Name: "same", + Endpoints: []tokens3.Endpoint{ + tokens3.Endpoint{ + ID: "1", + Region: "same", + Interface: "public", + URL: "https://public.correct.com/", + }, + tokens3.Endpoint{ + ID: "2", + Region: "same", + Interface: "admin", + URL: "https://admin.correct.com/", + }, + tokens3.Endpoint{ + ID: "3", + Region: "same", + Interface: "internal", + URL: "https://internal.correct.com/", + }, + tokens3.Endpoint{ + ID: "4", + Region: "different", + Interface: "public", + URL: "https://badregion.com/", + }, + }, + }, + tokens3.CatalogEntry{ + Type: "same", + Name: "different", + Endpoints: []tokens3.Endpoint{ + tokens3.Endpoint{ + ID: "5", + Region: "same", + Interface: "public", + URL: "https://badname.com/", + }, + tokens3.Endpoint{ + ID: "6", + Region: "different", + Interface: "public", + URL: "https://badname.com/+badregion", + }, + }, + }, + tokens3.CatalogEntry{ + Type: "different", + Name: "different", + Endpoints: []tokens3.Endpoint{ + tokens3.Endpoint{ + ID: "7", + Region: "same", + Interface: "public", + URL: "https://badtype.com/+badname", + }, + tokens3.Endpoint{ + ID: "8", + Region: "different", + Interface: "public", + URL: "https://badtype.com/+badregion+badname", + }, + }, + }, + }, +} + +func TestV3EndpointExact(t *testing.T) { + expectedURLs := map[gophercloud.Availability]string{ + gophercloud.AvailabilityPublic: "https://public.correct.com/", + gophercloud.AvailabilityAdmin: "https://admin.correct.com/", + gophercloud.AvailabilityInternal: "https://internal.correct.com/", + } + + for availability, expected := range expectedURLs { + actual, err := V3EndpointURL(&catalog3, gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: availability, + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, expected, actual) + } +} + +func TestV3EndpointNone(t *testing.T) { + _, err := V3EndpointURL(&catalog3, gophercloud.EndpointOpts{ + Type: "nope", + Availability: gophercloud.AvailabilityPublic, + }) + th.CheckEquals(t, gophercloud.ErrEndpointNotFound, err) +} + +func TestV3EndpointMultiple(t *testing.T) { + _, err := V3EndpointURL(&catalog3, gophercloud.EndpointOpts{ + Type: "same", + Region: "same", + Availability: gophercloud.AvailabilityPublic, + }) + if !strings.HasPrefix(err.Error(), "Discovered 2 matching endpoints:") { + t.Errorf("Received unexpected error: %v", err) + } +} + +func TestV3EndpointBadAvailability(t *testing.T) { + _, err := V3EndpointURL(&catalog3, gophercloud.EndpointOpts{ + Type: "same", + Name: "same", + Region: "same", + Availability: "wat", + }) + th.CheckEquals(t, "Unexpected availability in endpoint query: wat", err.Error()) +} 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 000000000000..895417871677 --- /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 000000000000..8256f0fe8e66 --- /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 000000000000..9a333140b2b6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles/requests.go @@ -0,0 +1,33 @@ +package roles + +import ( + "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 = client.Put(userRoleURL(client, tenantID, userID, roleID), nil, nil, nil) + 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 = client.Delete(userRoleURL(client, tenantID, userID, roleID), nil) + 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 000000000000..7bfeea44a810 --- /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 000000000000..ebb3aa530b87 --- /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 000000000000..61b31551dd97 --- /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/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate.go new file mode 100644 index 000000000000..fd6e80ea6f16 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate.go @@ -0,0 +1,52 @@ +package extensions + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtensionPage is a single page of Extension results. +type ExtensionPage struct { + common.ExtensionPage +} + +// IsEmpty returns true if the current page contains at least one Extension. +func (page ExtensionPage) IsEmpty() (bool, error) { + is, err := ExtractExtensions(page) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the +// elements into a slice of Extension structs. +func ExtractExtensions(page pagination.Page) ([]common.Extension, error) { + // Identity v2 adds an intermediate "values" object. + + var resp struct { + Extensions struct { + Values []common.Extension `mapstructure:"values"` + } `mapstructure:"extensions"` + } + + err := mapstructure.Decode(page.(ExtensionPage).Body, &resp) + return resp.Extensions.Values, err +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) common.GetResult { + return common.Get(c, alias) +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c).WithPageCreator(func(r pagination.PageResult) pagination.Page { + return ExtensionPage{ + ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)}, + } + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate_test.go new file mode 100644 index 000000000000..504118a825ba --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/delegate_test.go @@ -0,0 +1,38 @@ +package extensions + +import ( + "testing" + + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "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() + HandleListExtensionsSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, common.ExpectedExtensions, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + common.HandleGetExtensionSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, common.SingleExtension, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/doc.go new file mode 100644 index 000000000000..791e4e391da3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the +// different extensions available for the OpenStack Identity service. +package extensions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/fixtures.go new file mode 100644 index 000000000000..96cb7d24a13a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/extensions/fixtures.go @@ -0,0 +1,60 @@ +// +build fixtures + +package extensions + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput provides a single Extension result. It differs from the delegated implementation +// by the introduction of an intermediate "values" member. +const ListOutput = ` +{ + "extensions": { + "values": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] + } +} +` + +// HandleListExtensionsSuccessfully creates an HTTP handler that returns ListOutput for a List +// call. +func HandleListExtensionsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ` +{ + "extensions": { + "values": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] + } +} + `) + }) + +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/doc.go new file mode 100644 index 000000000000..0c2d49d5670e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/doc.go @@ -0,0 +1,7 @@ +// Package tenants provides information and interaction with the +// tenants API resource for the OpenStack Identity service. +// +// See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +// and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants +// for more information. +package tenants diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/fixtures.go new file mode 100644 index 000000000000..7f044ac3b2d4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/fixtures.go @@ -0,0 +1,65 @@ +// +build fixtures + +package tenants + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +// ListOutput provides a single page of Tenant results. +const ListOutput = ` +{ + "tenants": [ + { + "id": "1234", + "name": "Red Team", + "description": "The team that is red", + "enabled": true + }, + { + "id": "9876", + "name": "Blue Team", + "description": "The team that is blue", + "enabled": false + } + ] +} +` + +// RedTeam is a Tenant fixture. +var RedTeam = Tenant{ + ID: "1234", + Name: "Red Team", + Description: "The team that is red", + Enabled: true, +} + +// BlueTeam is a Tenant fixture. +var BlueTeam = Tenant{ + ID: "9876", + Name: "Blue Team", + Description: "The team that is blue", + Enabled: false, +} + +// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput. +var ExpectedTenantSlice = []Tenant{RedTeam, BlueTeam} + +// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that +// responds with a list of two tenants. +func HandleListTenantsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListOutput) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests.go new file mode 100644 index 000000000000..5a359f5c9e27 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests.go @@ -0,0 +1,33 @@ +package tenants + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts filters the Tenants that are returned by the List call. +type ListOpts struct { + // Marker is the ID of the last Tenant on the previous page. + Marker string `q:"marker"` + + // Limit specifies the page size. + Limit int `q:"limit"` +} + +// List enumerates the Tenants to which the current token has access. +func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return TenantPage{pagination.LinkedPageBase{PageResult: r}} + } + + url := listURL(client) + if opts != nil { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + url += q.String() + } + + return pagination.NewPager(client, url, createPage) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests_test.go new file mode 100644 index 000000000000..e8f172dd1831 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/requests_test.go @@ -0,0 +1,29 @@ +package tenants + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListTenants(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTenantsSuccessfully(t) + + count := 0 + err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := ExtractTenants(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedTenantSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/results.go new file mode 100644 index 000000000000..c1220c384bcb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/results.go @@ -0,0 +1,62 @@ +package tenants + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Tenant is a grouping of users in the identity service. +type Tenant struct { + // ID is a unique identifier for this tenant. + ID string `mapstructure:"id"` + + // Name is a friendlier user-facing name for this tenant. + Name string `mapstructure:"name"` + + // Description is a human-readable explanation of this Tenant's purpose. + Description string `mapstructure:"description"` + + // Enabled indicates whether or not a tenant is active. + Enabled bool `mapstructure:"enabled"` +} + +// TenantPage is a single page of Tenant results. +type TenantPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (page TenantPage) IsEmpty() (bool, error) { + tenants, err := ExtractTenants(page) + if err != nil { + return false, err + } + return len(tenants) == 0, nil +} + +// NextPageURL extracts the "next" link from the tenants_links section of the result. +func (page TenantPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"tenants_links"` + } + + var r resp + err := mapstructure.Decode(page.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// ExtractTenants returns a slice of Tenants contained in a single page of results. +func ExtractTenants(page pagination.Page) ([]Tenant, error) { + casted := page.(TenantPage).Body + var response struct { + Tenants []Tenant `mapstructure:"tenants"` + } + + err := mapstructure.Decode(casted, &response) + return response.Tenants, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/urls.go new file mode 100644 index 000000000000..1dd6ce023f9e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tenants/urls.go @@ -0,0 +1,7 @@ +package tenants + +import "github.com/rackspace/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/doc.go new file mode 100644 index 000000000000..31cacc5e17bd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/doc.go @@ -0,0 +1,5 @@ +// Package tokens provides information and interaction with the token API +// resource for the OpenStack Identity service. +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +package tokens diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go new file mode 100644 index 000000000000..3a9172e0cc79 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/errors.go @@ -0,0 +1,30 @@ +package tokens + +import ( + "errors" + "fmt" +) + +var ( + // ErrUserIDProvided is returned if you attempt to authenticate with a UserID. + ErrUserIDProvided = unacceptedAttributeErr("UserID") + + // ErrAPIKeyProvided is returned if you attempt to authenticate with an APIKey. + ErrAPIKeyProvided = unacceptedAttributeErr("APIKey") + + // ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID. + ErrDomainIDProvided = unacceptedAttributeErr("DomainID") + + // ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName. + ErrDomainNameProvided = unacceptedAttributeErr("DomainName") + + // ErrUsernameRequired is returned if you attempt ot authenticate without a Username. + ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.") + + // ErrPasswordRequired is returned if you don't provide a password. + ErrPasswordRequired = errors.New("Please supply a Password in your AuthOptions.") +) + +func unacceptedAttributeErr(attribute string) error { + return fmt.Errorf("The base Identity V2 API does not accept authentication by %s", attribute) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/fixtures.go new file mode 100644 index 000000000000..1cb0d0527b98 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/fixtures.go @@ -0,0 +1,128 @@ +// +build fixtures + +package tokens + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" + th "github.com/rackspace/gophercloud/testhelper" +) + +// ExpectedToken is the token that should be parsed from TokenCreationResponse. +var ExpectedToken = &Token{ + ID: "aaaabbbbccccdddd", + ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC), + Tenant: tenants.Tenant{ + ID: "fc394f2ab2df4114bde39905f800dc57", + Name: "test", + Description: "There are many tenants. This one is yours.", + Enabled: true, + }, +} + +// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse. +var ExpectedServiceCatalog = &ServiceCatalog{ + Entries: []CatalogEntry{ + CatalogEntry{ + Name: "inscrutablewalrus", + Type: "something", + Endpoints: []Endpoint{ + Endpoint{ + PublicURL: "http://something0:1234/v2/", + Region: "region0", + }, + Endpoint{ + PublicURL: "http://something1:1234/v2/", + Region: "region1", + }, + }, + }, + CatalogEntry{ + Name: "arbitrarypenguin", + Type: "else", + Endpoints: []Endpoint{ + Endpoint{ + PublicURL: "http://else0:4321/v3/", + Region: "region0", + }, + }, + }, + }, +} + +// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog. +const TokenCreationResponse = ` +{ + "access": { + "token": { + "issued_at": "2014-01-30T15:30:58.000000Z", + "expires": "2014-01-31T15:30:58Z", + "id": "aaaabbbbccccdddd", + "tenant": { + "description": "There are many tenants. This one is yours.", + "enabled": true, + "id": "fc394f2ab2df4114bde39905f800dc57", + "name": "test" + } + }, + "serviceCatalog": [ + { + "endpoints": [ + { + "publicURL": "http://something0:1234/v2/", + "region": "region0" + }, + { + "publicURL": "http://something1:1234/v2/", + "region": "region1" + } + ], + "type": "something", + "name": "inscrutablewalrus" + }, + { + "endpoints": [ + { + "publicURL": "http://else0:4321/v3/", + "region": "region0" + } + ], + "type": "else", + "name": "arbitrarypenguin" + } + ] + } +} +` + +// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been +// constructed properly given certain auth options, and returns the result. +func HandleTokenPost(t *testing.T, requestJSON string) { + th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + if requestJSON != "" { + th.TestJSONRequest(t, r, requestJSON) + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, TokenCreationResponse) + }) +} + +// IsSuccessful ensures that a CreateResult was successful and contains the correct token and +// service catalog. +func IsSuccessful(t *testing.T, result CreateResult) { + token, err := result.ExtractToken() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedToken, token) + + serviceCatalog, err := result.ExtractServiceCatalog() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go new file mode 100644 index 000000000000..efa054fb3994 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests.go @@ -0,0 +1,82 @@ +package tokens + +import "github.com/rackspace/gophercloud" + +// AuthOptionsBuilder describes any argument that may be passed to the Create call. +type AuthOptionsBuilder interface { + + // ToTokenCreateMap assembles the Create request body, returning an error if parameters are + // missing or inconsistent. + ToTokenCreateMap() (map[string]interface{}, error) +} + +// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder +// interface. +type AuthOptions struct { + gophercloud.AuthOptions +} + +// WrapOptions embeds a root AuthOptions struct in a package-specific one. +func WrapOptions(original gophercloud.AuthOptions) AuthOptions { + return AuthOptions{AuthOptions: original} +} + +// ToTokenCreateMap converts AuthOptions into nested maps that can be serialized into a JSON +// request. +func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) { + // Error out if an unsupported auth option is present. + if auth.UserID != "" { + return nil, ErrUserIDProvided + } + if auth.APIKey != "" { + return nil, ErrAPIKeyProvided + } + if auth.DomainID != "" { + return nil, ErrDomainIDProvided + } + if auth.DomainName != "" { + return nil, ErrDomainNameProvided + } + + // Username and Password are always required. + if auth.Username == "" { + return nil, ErrUsernameRequired + } + if auth.Password == "" { + return nil, ErrPasswordRequired + } + + // Populate the request map. + authMap := make(map[string]interface{}) + + authMap["passwordCredentials"] = map[string]interface{}{ + "username": auth.Username, + "password": auth.Password, + } + + if auth.TenantID != "" { + authMap["tenantId"] = auth.TenantID + } + if auth.TenantName != "" { + authMap["tenantName"] = auth.TenantName + } + + return map[string]interface{}{"auth": authMap}, nil +} + +// Create authenticates to the identity service and attempts to acquire a Token. +// If successful, the CreateResult +// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(), +// which abstracts all of the gory details about navigating service catalogs and such. +func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) CreateResult { + request, err := auth.ToTokenCreateMap() + if err != nil { + return CreateResult{gophercloud.Result{Err: err}} + } + + var result CreateResult + _, result.Err = client.Post(CreateURL(client), request, &result.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + return result +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go new file mode 100644 index 000000000000..2f02825a47aa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/requests_test.go @@ -0,0 +1,140 @@ +package tokens + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) CreateResult { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleTokenPost(t, requestJSON) + + return Create(client.ServiceClient(), AuthOptions{options}) +} + +func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleTokenPost(t, "") + + actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err + th.CheckEquals(t, expectedErr, actualErr) +} + +func TestCreateWithPassword(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "swordfish", + } + + IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "passwordCredentials": { + "username": "me", + "password": "swordfish" + } + } + } + `)) +} + +func TestCreateTokenWithTenantID(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "opensesame", + TenantID: "fc394f2ab2df4114bde39905f800dc57", + } + + IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "tenantId": "fc394f2ab2df4114bde39905f800dc57", + "passwordCredentials": { + "username": "me", + "password": "opensesame" + } + } + } + `)) +} + +func TestCreateTokenWithTenantName(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "opensesame", + TenantName: "demo", + } + + IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "tenantName": "demo", + "passwordCredentials": { + "username": "me", + "password": "opensesame" + } + } + } + `)) +} + +func TestProhibitUserID(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + UserID: "1234", + Password: "thing", + } + + tokenPostErr(t, options, ErrUserIDProvided) +} + +func TestProhibitAPIKey(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "thing", + APIKey: "123412341234", + } + + tokenPostErr(t, options, ErrAPIKeyProvided) +} + +func TestProhibitDomainID(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "thing", + DomainID: "1234", + } + + tokenPostErr(t, options, ErrDomainIDProvided) +} + +func TestProhibitDomainName(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + Password: "thing", + DomainName: "wat", + } + + tokenPostErr(t, options, ErrDomainNameProvided) +} + +func TestRequireUsername(t *testing.T) { + options := gophercloud.AuthOptions{ + Password: "thing", + } + + tokenPostErr(t, options, ErrUsernameRequired) +} + +func TestRequirePassword(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + } + + tokenPostErr(t, options, ErrPasswordRequired) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/results.go new file mode 100644 index 000000000000..1eddb9d56442 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/results.go @@ -0,0 +1,133 @@ +package tokens + +import ( + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" +) + +// Token provides only the most basic information related to an authentication token. +type Token struct { + // ID provides the primary means of identifying a user to the OpenStack API. + // OpenStack defines this field as an opaque value, so do not depend on its content. + // It is safe, however, to compare for equality. + ID string + + // ExpiresAt provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid. + // After this point in time, future API requests made using this authentication token will respond with errors. + // Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication. + // See the AuthOptions structure for more details. + ExpiresAt time.Time + + // Tenant provides information about the tenant to which this token grants access. + Tenant tenants.Tenant +} + +// Endpoint represents a single API endpoint offered by a service. +// It provides the public and internal URLs, if supported, along with a region specifier, again if provided. +// The significance of the Region field will depend upon your provider. +// +// In addition, the interface offered by the service will have version information associated with it +// through the VersionId, VersionInfo, and VersionList fields, if provided or supported. +// +// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value (""). +type Endpoint struct { + TenantID string `mapstructure:"tenantId"` + PublicURL string `mapstructure:"publicURL"` + InternalURL string `mapstructure:"internalURL"` + AdminURL string `mapstructure:"adminURL"` + Region string `mapstructure:"region"` + VersionID string `mapstructure:"versionId"` + VersionInfo string `mapstructure:"versionInfo"` + VersionList string `mapstructure:"versionList"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V2 service catalog listing. +// Each class of service, such as cloud DNS or block storage services, will have a single +// CatalogEntry representing it. +// +// Note: when looking for the desired service, try, whenever possible, to key off the type field. +// Otherwise, you'll tie the representation of the service to a specific provider. +type CatalogEntry struct { + // Name will contain the provider-specified name for the service. + Name string `mapstructure:"name"` + + // Type will contain a type string if OpenStack defines a type for the service. + // Otherwise, for provider-specific services, the provider may assign their own type strings. + Type string `mapstructure:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that may exist for + // the service. + Endpoints []Endpoint `mapstructure:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry +} + +// CreateResult defers the interpretation of a created token. +// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog. +type CreateResult struct { + gophercloud.Result +} + +// ExtractToken returns the just-created Token from a CreateResult. +func (result CreateResult) ExtractToken() (*Token, error) { + if result.Err != nil { + return nil, result.Err + } + + var response struct { + Access struct { + Token struct { + Expires string `mapstructure:"expires"` + ID string `mapstructure:"id"` + Tenant tenants.Tenant `mapstructure:"tenant"` + } `mapstructure:"token"` + } `mapstructure:"access"` + } + + err := mapstructure.Decode(result.Body, &response) + if err != nil { + return nil, err + } + + expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires) + if err != nil { + return nil, err + } + + return &Token{ + ID: response.Access.Token.ID, + ExpiresAt: expiresTs, + Tenant: response.Access.Token.Tenant, + }, nil +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token. +func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + if result.Err != nil { + return nil, result.Err + } + + var response struct { + Access struct { + Entries []CatalogEntry `mapstructure:"serviceCatalog"` + } `mapstructure:"access"` + } + + err := mapstructure.Decode(result.Body, &response) + if err != nil { + return nil, err + } + + return &ServiceCatalog{Entries: response.Access.Entries}, nil +} + +// createErr quickly packs an error in a CreateResult. +func createErr(err error) CreateResult { + return CreateResult{gophercloud.Result{Err: err}} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/urls.go new file mode 100644 index 000000000000..cd4c696c7a7c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/tokens/urls.go @@ -0,0 +1,8 @@ +package tokens + +import "github.com/rackspace/gophercloud" + +// CreateURL generates the URL used to create new Tokens. +func CreateURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tokens") +} 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 000000000000..82abcb9fccbe --- /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 000000000000..8941868dd2bf --- /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 000000000000..88be45ecc018 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v2/users/requests.go @@ -0,0 +1,161 @@ +package users + +import ( + "errors" + + "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 = client.Post(rootURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{ + 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 = client.Get(ResourceURL(client, id), &result.Body, nil) + 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 + reqBody := opts.ToUserUpdateMap() + _, result.Err = client.Put(ResourceURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{ + 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 = client.Delete(ResourceURL(client, id), nil) + 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 000000000000..04f837163abb --- /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 000000000000..f531d5d023a3 --- /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 000000000000..7ec4385d743b --- /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/endpoints/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/doc.go new file mode 100644 index 000000000000..85163949a831 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/doc.go @@ -0,0 +1,6 @@ +// Package endpoints provides information and interaction with the service +// endpoints API resource in the OpenStack Identity service. +// +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v3.html#endpoints-v3 +package endpoints diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/errors.go new file mode 100644 index 000000000000..854957ff98d5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/errors.go @@ -0,0 +1,21 @@ +package endpoints + +import "fmt" + +func requiredAttribute(attribute string) error { + return fmt.Errorf("You must specify %s for this endpoint.", attribute) +} + +var ( + // ErrAvailabilityRequired is reported if an Endpoint is created without an Availability. + ErrAvailabilityRequired = requiredAttribute("an availability") + + // ErrNameRequired is reported if an Endpoint is created without a Name. + ErrNameRequired = requiredAttribute("a name") + + // ErrURLRequired is reported if an Endpoint is created without a URL. + ErrURLRequired = requiredAttribute("a URL") + + // ErrServiceIDRequired is reported if an Endpoint is created without a ServiceID. + ErrServiceIDRequired = requiredAttribute("a serviceID") +) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests.go new file mode 100644 index 000000000000..99a495d594ec --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests.go @@ -0,0 +1,123 @@ +package endpoints + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// EndpointOpts contains the subset of Endpoint attributes that should be used to create or update an Endpoint. +type EndpointOpts struct { + Availability gophercloud.Availability + Name string + Region string + URL string + ServiceID string +} + +// Create inserts a new Endpoint into the service catalog. +// Within EndpointOpts, Region may be omitted by being left as "", but all other fields are required. +func Create(client *gophercloud.ServiceClient, opts EndpointOpts) CreateResult { + // Redefined so that Region can be re-typed as a *string, which can be omitted from the JSON output. + type endpoint struct { + Interface string `json:"interface"` + Name string `json:"name"` + Region *string `json:"region,omitempty"` + URL string `json:"url"` + ServiceID string `json:"service_id"` + } + + type request struct { + Endpoint endpoint `json:"endpoint"` + } + + // Ensure that EndpointOpts is fully populated. + if opts.Availability == "" { + return createErr(ErrAvailabilityRequired) + } + if opts.Name == "" { + return createErr(ErrNameRequired) + } + if opts.URL == "" { + return createErr(ErrURLRequired) + } + if opts.ServiceID == "" { + return createErr(ErrServiceIDRequired) + } + + // Populate the request body. + reqBody := request{ + Endpoint: endpoint{ + Interface: string(opts.Availability), + Name: opts.Name, + URL: opts.URL, + ServiceID: opts.ServiceID, + }, + } + reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region) + + var result CreateResult + _, result.Err = client.Post(listURL(client), reqBody, &result.Body, nil) + return result +} + +// ListOpts allows finer control over the endpoints returned by a List call. +// All fields are optional. +type ListOpts struct { + Availability gophercloud.Availability `q:"interface"` + ServiceID string `q:"service_id"` + Page int `q:"page"` + PerPage int `q:"per_page"` +} + +// List enumerates endpoints in a paginated collection, optionally filtered by ListOpts criteria. +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + u := listURL(client) + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + createPage := func(r pagination.PageResult) pagination.Page { + return EndpointPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, u, createPage) +} + +// Update changes an existing endpoint with new data. +// All fields are optional in the provided EndpointOpts. +func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) UpdateResult { + type endpoint struct { + Interface *string `json:"interface,omitempty"` + Name *string `json:"name,omitempty"` + Region *string `json:"region,omitempty"` + URL *string `json:"url,omitempty"` + ServiceID *string `json:"service_id,omitempty"` + } + + type request struct { + Endpoint endpoint `json:"endpoint"` + } + + reqBody := request{Endpoint: endpoint{}} + reqBody.Endpoint.Interface = gophercloud.MaybeString(string(opts.Availability)) + reqBody.Endpoint.Name = gophercloud.MaybeString(opts.Name) + reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region) + reqBody.Endpoint.URL = gophercloud.MaybeString(opts.URL) + reqBody.Endpoint.ServiceID = gophercloud.MaybeString(opts.ServiceID) + + var result UpdateResult + _, result.Err = client.Request("PATCH", endpointURL(client, endpointID), gophercloud.RequestOpts{ + JSONBody: &reqBody, + JSONResponse: &result.Body, + OkCodes: []int{200}, + }) + return result +} + +// Delete removes an endpoint from the service catalog. +func Delete(client *gophercloud.ServiceClient, endpointID string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(endpointURL(client, endpointID), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests_test.go new file mode 100644 index 000000000000..80687c4cb701 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/requests_test.go @@ -0,0 +1,226 @@ +package endpoints + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestCreateSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "POST") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, ` + { + "endpoint": { + "interface": "public", + "name": "the-endiest-of-points", + "region": "underground", + "url": "https://1.2.3.4:9000/", + "service_id": "asdfasdfasdfasdf" + } + } + `) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, ` + { + "endpoint": { + "id": "12", + "interface": "public", + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "the-endiest-of-points", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + } + } + `) + }) + + actual, err := Create(client.ServiceClient(), EndpointOpts{ + Availability: gophercloud.AvailabilityPublic, + Name: "the-endiest-of-points", + Region: "underground", + URL: "https://1.2.3.4:9000/", + ServiceID: "asdfasdfasdfasdf", + }).Extract() + if err != nil { + t.Fatalf("Unable to create an endpoint: %v", err) + } + + expected := &Endpoint{ + ID: "12", + Availability: gophercloud.AvailabilityPublic, + Name: "the-endiest-of-points", + Region: "underground", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9000/", + } + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Expected %#v, was %#v", expected, actual) + } +} + +func TestListEndpoints(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "endpoints": [ + { + "id": "12", + "interface": "public", + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "the-endiest-of-points", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + }, + { + "id": "13", + "interface": "internal", + "links": { + "self": "https://localhost:5000/v3/endpoints/13" + }, + "name": "shhhh", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9001/" + } + ], + "links": { + "next": null, + "previous": null + } + } + `) + }) + + count := 0 + List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractEndpoints(page) + if err != nil { + t.Errorf("Failed to extract endpoints: %v", err) + return false, err + } + + expected := []Endpoint{ + Endpoint{ + ID: "12", + Availability: gophercloud.AvailabilityPublic, + Name: "the-endiest-of-points", + Region: "underground", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9000/", + }, + Endpoint{ + ID: "13", + Availability: gophercloud.AvailabilityInternal, + Name: "shhhh", + Region: "underground", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9001/", + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } + + return true, nil + }) + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestUpdateEndpoint(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "PATCH") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, ` + { + "endpoint": { + "name": "renamed", + "region": "somewhere-else" + } + } + `) + + fmt.Fprintf(w, ` + { + "endpoint": { + "id": "12", + "interface": "public", + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "renamed", + "region": "somewhere-else", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + } + } + `) + }) + + actual, err := Update(client.ServiceClient(), "12", EndpointOpts{ + Name: "renamed", + Region: "somewhere-else", + }).Extract() + if err != nil { + t.Fatalf("Unexpected error from Update: %v", err) + } + + expected := &Endpoint{ + ID: "12", + Availability: gophercloud.AvailabilityPublic, + Name: "renamed", + Region: "somewhere-else", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9000/", + } + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, was %#v", expected, actual) + } +} + +func TestDeleteEndpoint(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/endpoints/34", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "DELETE") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(client.ServiceClient(), "34") + testhelper.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/results.go new file mode 100644 index 000000000000..128112295a22 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/results.go @@ -0,0 +1,82 @@ +package endpoints + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Endpoint. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Endpoint, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Endpoint `json:"endpoint"` + } + + err := mapstructure.Decode(r.Body, &res) + + return &res.Endpoint, err +} + +// CreateResult is the deferred result of a Create call. +type CreateResult struct { + commonResult +} + +// createErr quickly wraps an error in a CreateResult. +func createErr(err error) CreateResult { + return CreateResult{commonResult{gophercloud.Result{Err: err}}} +} + +// UpdateResult is the deferred result of an Update call. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the deferred result of an Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Endpoint describes the entry point for another service's API. +type Endpoint struct { + ID string `mapstructure:"id" json:"id"` + Availability gophercloud.Availability `mapstructure:"interface" json:"interface"` + Name string `mapstructure:"name" json:"name"` + Region string `mapstructure:"region" json:"region"` + ServiceID string `mapstructure:"service_id" json:"service_id"` + URL string `mapstructure:"url" json:"url"` +} + +// EndpointPage is a single page of Endpoint results. +type EndpointPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if no Endpoints were returned. +func (p EndpointPage) IsEmpty() (bool, error) { + es, err := ExtractEndpoints(p) + if err != nil { + return true, err + } + return len(es) == 0, nil +} + +// ExtractEndpoints extracts an Endpoint slice from a Page. +func ExtractEndpoints(page pagination.Page) ([]Endpoint, error) { + var response struct { + Endpoints []Endpoint `mapstructure:"endpoints"` + } + + err := mapstructure.Decode(page.(EndpointPage).Body, &response) + + return response.Endpoints, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls.go new file mode 100644 index 000000000000..547d7b102a5c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls.go @@ -0,0 +1,11 @@ +package endpoints + +import "github.com/rackspace/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("endpoints") +} + +func endpointURL(client *gophercloud.ServiceClient, endpointID string) string { + return client.ServiceURL("endpoints", endpointID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls_test.go new file mode 100644 index 000000000000..0b183b7434aa --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/endpoints/urls_test.go @@ -0,0 +1,23 @@ +package endpoints + +import ( + "testing" + + "github.com/rackspace/gophercloud" +) + +func TestGetListURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := listURL(&client) + if url != "http://localhost:5000/v3/endpoints" { + t.Errorf("Unexpected list URL generated: [%s]", url) + } +} + +func TestGetEndpointURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := endpointURL(&client, "1234") + if url != "http://localhost:5000/v3/endpoints/1234" { + t.Errorf("Unexpected service URL generated: [%s]", url) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/doc.go new file mode 100644 index 000000000000..fa56411856bd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/doc.go @@ -0,0 +1,3 @@ +// Package services provides information and interaction with the services API +// resource for the OpenStack Identity service. +package services diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests.go new file mode 100644 index 000000000000..3ee924f3ee69 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests.go @@ -0,0 +1,77 @@ +package services + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type response struct { + Service Service `json:"service"` +} + +// Create adds a new service of the requested type to the catalog. +func Create(client *gophercloud.ServiceClient, serviceType string) CreateResult { + type request struct { + Type string `json:"type"` + } + + req := request{Type: serviceType} + + var result CreateResult + _, result.Err = client.Post(listURL(client), req, &result.Body, nil) + return result +} + +// ListOpts allows you to query the List method. +type ListOpts struct { + ServiceType string `q:"type"` + PerPage int `q:"perPage"` + Page int `q:"page"` +} + +// List enumerates the services available to a specific user. +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + u := listURL(client) + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + createPage := func(r pagination.PageResult) pagination.Page { + return ServicePage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, u, createPage) +} + +// Get returns additional information about a service, given its ID. +func Get(client *gophercloud.ServiceClient, serviceID string) GetResult { + var result GetResult + _, result.Err = client.Get(serviceURL(client, serviceID), &result.Body, nil) + return result +} + +// Update changes the service type of an existing service. +func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) UpdateResult { + type request struct { + Type string `json:"type"` + } + + req := request{Type: serviceType} + + var result UpdateResult + _, result.Err = client.Request("PATCH", serviceURL(client, serviceID), gophercloud.RequestOpts{ + JSONBody: &req, + JSONResponse: &result.Body, + OkCodes: []int{200}, + }) + return result +} + +// Delete removes an existing service. +// It either deletes all associated endpoints, or fails until all endpoints are deleted. +func Delete(client *gophercloud.ServiceClient, serviceID string) DeleteResult { + var res DeleteResult + _, res.Err = client.Delete(serviceURL(client, serviceID), nil) + return res +} 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 new file mode 100644 index 000000000000..42f05d365a5b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/requests_test.go @@ -0,0 +1,209 @@ +package services + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestCreateSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "POST") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, `{ "type": "compute" }`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "service": { + "description": "Here's your service", + "id": "1234", + "name": "InscrutableOpenStackProjectName", + "type": "compute" + } + }`) + }) + + result, err := Create(client.ServiceClient(), "compute").Extract() + if err != nil { + t.Fatalf("Unexpected error from Create: %v", err) + } + + if result.Description == nil || *result.Description != "Here's your service" { + t.Errorf("Service description was unexpected [%s]", *result.Description) + } + if result.ID != "1234" { + t.Errorf("Service ID was unexpected [%s]", result.ID) + } + if result.Name != "InscrutableOpenStackProjectName" { + t.Errorf("Service name was unexpected [%s]", result.Name) + } + if result.Type != "compute" { + t.Errorf("Service type was unexpected [%s]", result.Type) + } +} + +func TestListSinglePage(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "links": { + "next": null, + "previous": null + }, + "services": [ + { + "description": "Service One", + "id": "1234", + "name": "service-one", + "type": "identity" + }, + { + "description": "Service Two", + "id": "9876", + "name": "service-two", + "type": "compute" + } + ] + } + `) + }) + + count := 0 + err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractServices(page) + if err != nil { + return false, err + } + + desc0 := "Service One" + desc1 := "Service Two" + expected := []Service{ + Service{ + Description: &desc0, + ID: "1234", + Name: "service-one", + Type: "identity", + }, + Service{ + Description: &desc1, + ID: "9876", + Name: "service-two", + Type: "compute", + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error while paging: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "service": { + "description": "Service One", + "id": "12345", + "name": "service-one", + "type": "identity" + } + } + `) + }) + + result, err := Get(client.ServiceClient(), "12345").Extract() + if err != nil { + t.Fatalf("Error fetching service information: %v", err) + } + + if result.ID != "12345" { + t.Errorf("Unexpected service ID: %s", result.ID) + } + if *result.Description != "Service One" { + t.Errorf("Unexpected service description: [%s]", *result.Description) + } + if result.Name != "service-one" { + t.Errorf("Unexpected service name: [%s]", result.Name) + } + if result.Type != "identity" { + t.Errorf("Unexpected service type: [%s]", result.Type) + } +} + +func TestUpdateSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "PATCH") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, `{ "type": "lasermagic" }`) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "service": { + "id": "12345", + "type": "lasermagic" + } + } + `) + }) + + result, err := Update(client.ServiceClient(), "12345", "lasermagic").Extract() + if err != nil { + t.Fatalf("Unable to update service: %v", err) + } + + if result.ID != "12345" { + t.Fatalf("Expected ID 12345, was %s", result.ID) + } +} + +func TestDeleteSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "DELETE") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(client.ServiceClient(), "12345") + testhelper.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/results.go new file mode 100644 index 000000000000..1d0d14128067 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/results.go @@ -0,0 +1,80 @@ +package services + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Service, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Service `json:"service"` + } + + err := mapstructure.Decode(r.Body, &res) + + return &res.Service, err +} + +// CreateResult is the deferred result of a Create call. +type CreateResult struct { + commonResult +} + +// GetResult is the deferred result of a Get call. +type GetResult struct { + commonResult +} + +// UpdateResult is the deferred result of an Update call. +type UpdateResult struct { + commonResult +} + +// DeleteResult is the deferred result of an Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Service is the result of a list or information query. +type Service struct { + Description *string `json:"description,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +// ServicePage is a single page of Service results. +type ServicePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (p ServicePage) IsEmpty() (bool, error) { + services, err := ExtractServices(p) + if err != nil { + return true, err + } + return len(services) == 0, nil +} + +// ExtractServices extracts a slice of Services from a Collection acquired from List. +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 +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls.go new file mode 100644 index 000000000000..85443a48a094 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls.go @@ -0,0 +1,11 @@ +package services + +import "github.com/rackspace/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("services") +} + +func serviceURL(client *gophercloud.ServiceClient, serviceID string) string { + return client.ServiceURL("services", serviceID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls_test.go new file mode 100644 index 000000000000..5a31b32316cf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/services/urls_test.go @@ -0,0 +1,23 @@ +package services + +import ( + "testing" + + "github.com/rackspace/gophercloud" +) + +func TestListURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := listURL(&client) + if url != "http://localhost:5000/v3/services" { + t.Errorf("Unexpected list URL generated: [%s]", url) + } +} + +func TestServiceURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := serviceURL(&client, "1234") + if url != "http://localhost:5000/v3/services/1234" { + t.Errorf("Unexpected service URL generated: [%s]", url) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/doc.go new file mode 100644 index 000000000000..76ff5f473873 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/doc.go @@ -0,0 +1,6 @@ +// Package tokens provides information and interaction with the token API +// resource for the OpenStack Identity service. +// +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3 +package tokens diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/errors.go new file mode 100644 index 000000000000..44761092bb98 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/errors.go @@ -0,0 +1,72 @@ +package tokens + +import ( + "errors" + "fmt" +) + +func unacceptedAttributeErr(attribute string) error { + return fmt.Errorf("The base Identity V3 API does not accept authentication by %s", attribute) +} + +func redundantWithTokenErr(attribute string) error { + return fmt.Errorf("%s may not be provided when authenticating with a TokenID", attribute) +} + +func redundantWithUserID(attribute string) error { + return fmt.Errorf("%s may not be provided when authenticating with a UserID", attribute) +} + +var ( + // ErrAPIKeyProvided indicates that an APIKey was provided but can't be used. + ErrAPIKeyProvided = unacceptedAttributeErr("APIKey") + + // ErrTenantIDProvided indicates that a TenantID was provided but can't be used. + ErrTenantIDProvided = unacceptedAttributeErr("TenantID") + + // ErrTenantNameProvided indicates that a TenantName was provided but can't be used. + ErrTenantNameProvided = unacceptedAttributeErr("TenantName") + + // ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead. + ErrUsernameWithToken = redundantWithTokenErr("Username") + + // ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead. + ErrUserIDWithToken = redundantWithTokenErr("UserID") + + // ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead. + ErrDomainIDWithToken = redundantWithTokenErr("DomainID") + + // ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s + ErrDomainNameWithToken = redundantWithTokenErr("DomainName") + + // ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once. + ErrUsernameOrUserID = errors.New("Exactly one of Username and UserID must be provided for password authentication") + + // ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used. + ErrDomainIDWithUserID = redundantWithUserID("DomainID") + + // ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used. + ErrDomainNameWithUserID = redundantWithUserID("DomainName") + + // ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it. + // It may also indicate that both a DomainID and a DomainName were provided at once. + ErrDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName to authenticate by Username") + + // ErrMissingPassword indicates that no password was provided and no token is available. + ErrMissingPassword = errors.New("You must provide a password to authenticate") + + // ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present. + ErrScopeDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName in a Scope with ProjectName") + + // ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope. + ErrScopeProjectIDOrProjectName = errors.New("You must provide at most one of ProjectID or ProjectName in a Scope") + + // ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope. + ErrScopeProjectIDAlone = errors.New("ProjectID must be supplied alone in a Scope") + + // ErrScopeDomainName indicates that a DomainName was provided alone in a Scope. + ErrScopeDomainName = errors.New("DomainName must be supplied with a ProjectName or ProjectID in a Scope.") + + // ErrScopeEmpty indicates that no credentials were provided in a Scope. + ErrScopeEmpty = errors.New("You must provide either a Project or Domain in a Scope") +) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go new file mode 100644 index 000000000000..d449ca36e89c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests.go @@ -0,0 +1,281 @@ +package tokens + +import ( + "net/http" + + "github.com/rackspace/gophercloud" +) + +// Scope allows a created token to be limited to a specific domain or project. +type Scope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string +} + +func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string { + h := c.AuthenticatedHeaders() + h["X-Subject-Token"] = subjectToken + return h +} + +// Create authenticates and either generates a new token, or changes the Scope of an existing token. +func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult { + type domainReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + } + + type projectReq struct { + Domain *domainReq `json:"domain,omitempty"` + Name *string `json:"name,omitempty"` + ID *string `json:"id,omitempty"` + } + + type userReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Password string `json:"password"` + Domain *domainReq `json:"domain,omitempty"` + } + + type passwordReq struct { + User userReq `json:"user"` + } + + type tokenReq struct { + ID string `json:"id"` + } + + type identityReq struct { + Methods []string `json:"methods"` + Password *passwordReq `json:"password,omitempty"` + Token *tokenReq `json:"token,omitempty"` + } + + type scopeReq struct { + Domain *domainReq `json:"domain,omitempty"` + Project *projectReq `json:"project,omitempty"` + } + + type authReq struct { + Identity identityReq `json:"identity"` + Scope *scopeReq `json:"scope,omitempty"` + } + + type request struct { + Auth authReq `json:"auth"` + } + + // Populate the request structure based on the provided arguments. Create and return an error + // if insufficient or incompatible information is present. + var req request + + // Test first for unrecognized arguments. + if options.APIKey != "" { + return createErr(ErrAPIKeyProvided) + } + if options.TenantID != "" { + return createErr(ErrTenantIDProvided) + } + if options.TenantName != "" { + return createErr(ErrTenantNameProvided) + } + + if options.Password == "" { + if c.TokenID != "" { + // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication + // parameters. + if options.Username != "" { + return createErr(ErrUsernameWithToken) + } + if options.UserID != "" { + return createErr(ErrUserIDWithToken) + } + if options.DomainID != "" { + return createErr(ErrDomainIDWithToken) + } + if options.DomainName != "" { + return createErr(ErrDomainNameWithToken) + } + + // Configure the request for Token authentication. + req.Auth.Identity.Methods = []string{"token"} + req.Auth.Identity.Token = &tokenReq{ + ID: c.TokenID, + } + } else { + // If no password or token ID are available, authentication can't continue. + return createErr(ErrMissingPassword) + } + } else { + // Password authentication. + req.Auth.Identity.Methods = []string{"password"} + + // At least one of Username and UserID must be specified. + if options.Username == "" && options.UserID == "" { + return createErr(ErrUsernameOrUserID) + } + + if options.Username != "" { + // If Username is provided, UserID may not be provided. + if options.UserID != "" { + return createErr(ErrUsernameOrUserID) + } + + // Either DomainID or DomainName must also be specified. + if options.DomainID == "" && options.DomainName == "" { + return createErr(ErrDomainIDOrDomainName) + } + + if options.DomainID != "" { + if options.DomainName != "" { + return createErr(ErrDomainIDOrDomainName) + } + + // Configure the request for Username and Password authentication with a DomainID. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &options.Username, + Password: options.Password, + Domain: &domainReq{ID: &options.DomainID}, + }, + } + } + + if options.DomainName != "" { + // Configure the request for Username and Password authentication with a DomainName. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &options.Username, + Password: options.Password, + Domain: &domainReq{Name: &options.DomainName}, + }, + } + } + } + + if options.UserID != "" { + // If UserID is specified, neither DomainID nor DomainName may be. + if options.DomainID != "" { + return createErr(ErrDomainIDWithUserID) + } + if options.DomainName != "" { + return createErr(ErrDomainNameWithUserID) + } + + // Configure the request for UserID and Password authentication. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ID: &options.UserID, Password: options.Password}, + } + } + } + + // Add a "scope" element if a Scope has been provided. + if scope != nil { + if scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if scope.DomainID == "" && scope.DomainName == "" { + return createErr(ErrScopeDomainIDOrDomainName) + } + if scope.ProjectID != "" { + return createErr(ErrScopeProjectIDOrProjectName) + } + + if scope.DomainID != "" { + // ProjectName + DomainID + req.Auth.Scope = &scopeReq{ + Project: &projectReq{ + Name: &scope.ProjectName, + Domain: &domainReq{ID: &scope.DomainID}, + }, + } + } + + if scope.DomainName != "" { + // ProjectName + DomainName + req.Auth.Scope = &scopeReq{ + Project: &projectReq{ + Name: &scope.ProjectName, + Domain: &domainReq{Name: &scope.DomainName}, + }, + } + } + } else if scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if scope.DomainID != "" { + return createErr(ErrScopeProjectIDAlone) + } + if scope.DomainName != "" { + return createErr(ErrScopeProjectIDAlone) + } + + // ProjectID + req.Auth.Scope = &scopeReq{ + Project: &projectReq{ID: &scope.ProjectID}, + } + } else if scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if scope.DomainName != "" { + return createErr(ErrScopeDomainIDOrDomainName) + } + + // DomainID + req.Auth.Scope = &scopeReq{ + Domain: &domainReq{ID: &scope.DomainID}, + } + } else if scope.DomainName != "" { + return createErr(ErrScopeDomainName) + } else { + return createErr(ErrScopeEmpty) + } + } + + var result CreateResult + var response *http.Response + response, result.Err = c.Post(tokenURL(c), req, &result.Body, nil) + if result.Err != nil { + return result + } + result.Header = response.Header + return result +} + +// Get validates and retrieves information about another token. +func Get(c *gophercloud.ServiceClient, token string) GetResult { + var result GetResult + var response *http.Response + response, result.Err = c.Get(tokenURL(c), &result.Body, &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{200, 203}, + }) + if result.Err != nil { + return result + } + result.Header = response.Header + return result +} + +// Validate determines if a specified token is valid or not. +func Validate(c *gophercloud.ServiceClient, token string) (bool, error) { + response, err := c.Request("HEAD", tokenURL(c), gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{204, 404}, + }) + if err != nil { + return false, err + } + + return response.StatusCode == 204, nil +} + +// Revoke immediately makes specified token invalid. +func Revoke(c *gophercloud.ServiceClient, token string) RevokeResult { + var res RevokeResult + _, res.Err = c.Delete(tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests_test.go new file mode 100644 index 000000000000..2b26e4ad3683 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/requests_test.go @@ -0,0 +1,514 @@ +package tokens + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper" +) + +// authTokenPost verifies that providing certain AuthOptions and Scope results in an expected JSON structure. +func authTokenPost(t *testing.T, options gophercloud.AuthOptions, scope *Scope, requestJSON string) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{ + TokenID: "12345abcdef", + }, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "POST") + testhelper.TestHeader(t, r, "Content-Type", "application/json") + testhelper.TestHeader(t, r, "Accept", "application/json") + testhelper.TestJSONRequest(t, r, requestJSON) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "token": { + "expires_at": "2014-10-02T13:45:00.000000Z" + } + }`) + }) + + _, err := Create(&client, options, scope).Extract() + if err != nil { + t.Errorf("Create returned an error: %v", err) + } +} + +func authTokenPostErr(t *testing.T, options gophercloud.AuthOptions, scope *Scope, includeToken bool, expectedErr error) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{}, + Endpoint: testhelper.Endpoint(), + } + if includeToken { + client.TokenID = "abcdef123456" + } + + _, err := Create(&client, options, scope).Extract() + if err == nil { + t.Errorf("Create did NOT return an error") + } + if err != expectedErr { + t.Errorf("Create returned an unexpected error: wanted %v, got %v", expectedErr, err) + } +} + +func TestCreateUserIDAndPassword(t *testing.T) { + authTokenPost(t, gophercloud.AuthOptions{UserID: "me", Password: "squirrel!"}, nil, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { "id": "me", "password": "squirrel!" } + } + } + } + } + `) +} + +func TestCreateUsernameDomainIDPassword(t *testing.T) { + authTokenPost(t, gophercloud.AuthOptions{Username: "fakey", Password: "notpassword", DomainID: "abc123"}, nil, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": { + "id": "abc123" + }, + "name": "fakey", + "password": "notpassword" + } + } + } + } + } + `) +} + +func TestCreateUsernameDomainNamePassword(t *testing.T) { + authTokenPost(t, gophercloud.AuthOptions{Username: "frank", Password: "swordfish", DomainName: "spork.net"}, nil, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": { + "name": "spork.net" + }, + "name": "frank", + "password": "swordfish" + } + } + } + } + } + `) +} + +func TestCreateTokenID(t *testing.T) { + authTokenPost(t, gophercloud.AuthOptions{}, nil, ` + { + "auth": { + "identity": { + "methods": ["token"], + "token": { + "id": "12345abcdef" + } + } + } + } + `) +} + +func TestCreateProjectIDScope(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &Scope{ProjectID: "123456"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "project": { + "id": "123456" + } + } + } + } + `) +} + +func TestCreateDomainIDScope(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &Scope{DomainID: "1000"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "domain": { + "id": "1000" + } + } + } + } + `) +} + +func TestCreateProjectNameAndDomainIDScope(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &Scope{ProjectName: "world-domination", DomainID: "1000"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "project": { + "domain": { + "id": "1000" + }, + "name": "world-domination" + } + } + } + } + `) +} + +func TestCreateProjectNameAndDomainNameScope(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"} + scope := &Scope{ProjectName: "world-domination", DomainName: "evil-plans"} + authTokenPost(t, options, scope, ` + { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "id": "fenris", + "password": "g0t0h311" + } + } + }, + "scope": { + "project": { + "domain": { + "name": "evil-plans" + }, + "name": "world-domination" + } + } + } + } + `) +} + +func TestCreateExtractsTokenFromResponse(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{}, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Subject-Token", "aaa111") + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "token": { + "expires_at": "2014-10-02T13:45:00.000000Z" + } + }`) + }) + + options := gophercloud.AuthOptions{UserID: "me", Password: "shhh"} + token, err := Create(&client, options, nil).Extract() + if err != nil { + t.Fatalf("Create returned an error: %v", err) + } + + if token.ID != "aaa111" { + t.Errorf("Expected token to be aaa111, but was %s", token.ID) + } +} + +func TestCreateFailureEmptyAuth(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{}, nil, false, ErrMissingPassword) +} + +func TestCreateFailureAPIKey(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{APIKey: "something"}, nil, false, ErrAPIKeyProvided) +} + +func TestCreateFailureTenantID(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{TenantID: "something"}, nil, false, ErrTenantIDProvided) +} + +func TestCreateFailureTenantName(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{TenantName: "something"}, nil, false, ErrTenantNameProvided) +} + +func TestCreateFailureTokenIDUsername(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{Username: "something"}, nil, true, ErrUsernameWithToken) +} + +func TestCreateFailureTokenIDUserID(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{UserID: "something"}, nil, true, ErrUserIDWithToken) +} + +func TestCreateFailureTokenIDDomainID(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{DomainID: "something"}, nil, true, ErrDomainIDWithToken) +} + +func TestCreateFailureTokenIDDomainName(t *testing.T) { + authTokenPostErr(t, gophercloud.AuthOptions{DomainName: "something"}, nil, true, ErrDomainNameWithToken) +} + +func TestCreateFailureMissingUser(t *testing.T) { + options := gophercloud.AuthOptions{Password: "supersecure"} + authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID) +} + +func TestCreateFailureBothUser(t *testing.T) { + options := gophercloud.AuthOptions{ + Password: "supersecure", + Username: "oops", + UserID: "redundancy", + } + authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID) +} + +func TestCreateFailureMissingDomain(t *testing.T) { + options := gophercloud.AuthOptions{ + Password: "supersecure", + Username: "notuniqueenough", + } + authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName) +} + +func TestCreateFailureBothDomain(t *testing.T) { + options := gophercloud.AuthOptions{ + Password: "supersecure", + Username: "someone", + DomainID: "hurf", + DomainName: "durf", + } + authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName) +} + +func TestCreateFailureUserIDDomainID(t *testing.T) { + options := gophercloud.AuthOptions{ + UserID: "100", + Password: "stuff", + DomainID: "oops", + } + authTokenPostErr(t, options, nil, false, ErrDomainIDWithUserID) +} + +func TestCreateFailureUserIDDomainName(t *testing.T) { + options := gophercloud.AuthOptions{ + UserID: "100", + Password: "sssh", + DomainName: "oops", + } + authTokenPostErr(t, options, nil, false, ErrDomainNameWithUserID) +} + +func TestCreateFailureScopeProjectNameAlone(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{ProjectName: "notenough"} + authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName) +} + +func TestCreateFailureScopeProjectNameAndID(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{ProjectName: "whoops", ProjectID: "toomuch", DomainID: "1234"} + authTokenPostErr(t, options, scope, false, ErrScopeProjectIDOrProjectName) +} + +func TestCreateFailureScopeProjectIDAndDomainID(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{ProjectID: "toomuch", DomainID: "notneeded"} + authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone) +} + +func TestCreateFailureScopeProjectIDAndDomainNAme(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{ProjectID: "toomuch", DomainName: "notneeded"} + authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone) +} + +func TestCreateFailureScopeDomainIDAndDomainName(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{DomainID: "toomuch", DomainName: "notneeded"} + authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName) +} + +func TestCreateFailureScopeDomainNameAlone(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{DomainName: "notenough"} + authTokenPostErr(t, options, scope, false, ErrScopeDomainName) +} + +func TestCreateFailureEmptyScope(t *testing.T) { + options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"} + scope := &Scope{} + authTokenPostErr(t, options, scope, false, ErrScopeEmpty) +} + +func TestGetRequest(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{ + TokenID: "12345abcdef", + }, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "Content-Type", "") + testhelper.TestHeader(t, r, "Accept", "application/json") + testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef") + testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { "token": { "expires_at": "2014-08-29T13:10:01.000000Z" } } + `) + }) + + token, err := Get(&client, "abcdef12345").Extract() + if err != nil { + t.Errorf("Info returned an error: %v", err) + } + + expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014") + if token.ExpiresAt != expected { + t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), token.ExpiresAt.Format(time.UnixDate)) + } +} + +func prepareAuthTokenHandler(t *testing.T, expectedMethod string, status int) gophercloud.ServiceClient { + client := gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{ + TokenID: "12345abcdef", + }, + Endpoint: testhelper.Endpoint(), + } + + testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, expectedMethod) + testhelper.TestHeader(t, r, "Content-Type", "") + testhelper.TestHeader(t, r, "Accept", "application/json") + testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef") + testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345") + + w.WriteHeader(status) + }) + + return client +} + +func TestValidateRequestSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "HEAD", http.StatusNoContent) + + ok, err := Validate(&client, "abcdef12345") + if err != nil { + t.Errorf("Unexpected error from Validate: %v", err) + } + + if !ok { + t.Errorf("Validate returned false for a valid token") + } +} + +func TestValidateRequestFailure(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "HEAD", http.StatusNotFound) + + ok, err := Validate(&client, "abcdef12345") + if err != nil { + t.Errorf("Unexpected error from Validate: %v", err) + } + + if ok { + t.Errorf("Validate returned true for an invalid token") + } +} + +func TestValidateRequestError(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "HEAD", http.StatusUnauthorized) + + _, err := Validate(&client, "abcdef12345") + if err == nil { + t.Errorf("Missing expected error from Validate") + } +} + +func TestRevokeRequestSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "DELETE", http.StatusNoContent) + + res := Revoke(&client, "abcdef12345") + testhelper.AssertNoErr(t, res.Err) +} + +func TestRevokeRequestError(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + client := prepareAuthTokenHandler(t, "DELETE", http.StatusNotFound) + + res := Revoke(&client, "abcdef12345") + if res.Err == nil { + t.Errorf("Missing expected error from Revoke") + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/results.go new file mode 100644 index 000000000000..d134f7d4d074 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/results.go @@ -0,0 +1,139 @@ +package tokens + +import ( + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" +) + +// Endpoint represents a single API endpoint offered by a service. +// It matches either a public, internal or admin URL. +// If supported, it contains a region specifier, again if provided. +// The significance of the Region field will depend upon your provider. +type Endpoint struct { + ID string `mapstructure:"id"` + Region string `mapstructure:"region"` + Interface string `mapstructure:"interface"` + URL string `mapstructure:"url"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V3 service catalog listing. +// Each class of service, such as cloud DNS or block storage services, could have multiple +// CatalogEntry representing it (one by interface type, e.g public, admin or internal). +// +// Note: when looking for the desired service, try, whenever possible, to key off the type field. +// Otherwise, you'll tie the representation of the service to a specific provider. +type CatalogEntry struct { + + // Service ID + ID string `mapstructure:"id"` + + // Name will contain the provider-specified name for the service. + Name string `mapstructure:"name"` + + // Type will contain a type string if OpenStack defines a type for the service. + // Otherwise, for provider-specific services, the provider may assign their own type strings. + Type string `mapstructure:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that may exist for + // the service. + Endpoints []Endpoint `mapstructure:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry +} + +// commonResult is the deferred result of a Create or a Get call. +type commonResult struct { + gophercloud.Result +} + +// Extract is a shortcut for ExtractToken. +// This function is deprecated and still present for backward compatibility. +func (r commonResult) Extract() (*Token, error) { + return r.ExtractToken() +} + +// ExtractToken interprets a commonResult as a Token. +func (r commonResult) ExtractToken() (*Token, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Token struct { + ExpiresAt string `mapstructure:"expires_at"` + } `mapstructure:"token"` + } + + var token Token + + // Parse the token itself from the stored headers. + token.ID = r.Header.Get("X-Subject-Token") + + err := mapstructure.Decode(r.Body, &response) + if err != nil { + return nil, err + } + + // Attempt to parse the timestamp. + token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt) + + return &token, err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token. +func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + if result.Err != nil { + return nil, result.Err + } + + var response struct { + Token struct { + Entries []CatalogEntry `mapstructure:"catalog"` + } `mapstructure:"token"` + } + + err := mapstructure.Decode(result.Body, &response) + if err != nil { + return nil, err + } + + return &ServiceCatalog{Entries: response.Token.Entries}, nil +} + +// CreateResult defers the interpretation of a created token. +// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog. +type CreateResult struct { + commonResult +} + +// createErr quickly creates a CreateResult that reports an error. +func createErr(err error) CreateResult { + return CreateResult{ + commonResult: commonResult{Result: gophercloud.Result{Err: err}}, + } +} + +// GetResult is the deferred response from a Get call. +type GetResult struct { + commonResult +} + +// RevokeResult is the deferred response from a Revoke call. +type RevokeResult struct { + commonResult +} + +// Token is a string that grants a user access to a controlled set of services in an OpenStack provider. +// Each Token is valid for a set length of time. +type Token struct { + // ID is the issued token. + ID string + + // ExpiresAt is the timestamp at which this token will no longer be accepted. + ExpiresAt time.Time +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls.go new file mode 100644 index 000000000000..360b60a82fba --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls.go @@ -0,0 +1,7 @@ +package tokens + +import "github.com/rackspace/gophercloud" + +func tokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls_test.go new file mode 100644 index 000000000000..549c398620a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/identity/v3/tokens/urls_test.go @@ -0,0 +1,21 @@ +package tokens + +import ( + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper" +) + +func TestTokenURL(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + client := gophercloud.ServiceClient{Endpoint: testhelper.Endpoint()} + + expected := testhelper.Endpoint() + "auth/tokens" + actual := tokenURL(&client) + if actual != expected { + t.Errorf("Expected URL %s, but was %s", expected, actual) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/doc.go new file mode 100644 index 000000000000..0208ee20ecb2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/doc.go @@ -0,0 +1,4 @@ +// Package apiversions provides information and interaction with the different +// API versions for the OpenStack Neutron service. This functionality is not +// restricted to this particular version. +package apiversions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/errors.go new file mode 100644 index 000000000000..76bdb14f7509 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/errors.go @@ -0,0 +1 @@ +package apiversions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests.go new file mode 100644 index 000000000000..9fb6de14110c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests.go @@ -0,0 +1,21 @@ +package apiversions + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListVersions lists all the Neutron API versions available to end-users +func ListVersions(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, apiVersionsURL(c), func(r pagination.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} + +// ListVersionResources lists all of the different API resources for a particular +// API versions. Typical resources for Neutron might be: networks, subnets, etc. +func ListVersionResources(c *gophercloud.ServiceClient, v string) pagination.Pager { + return pagination.NewPager(c, apiInfoURL(c, v), func(r pagination.PageResult) pagination.Page { + return APIVersionResourcePage{pagination.SinglePageBase(r)} + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests_test.go new file mode 100644 index 000000000000..d35af9f0c6b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/requests_test.go @@ -0,0 +1,182 @@ +package apiversions + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", 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, ` +{ + "versions": [ + { + "status": "CURRENT", + "id": "v2.0", + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0", + "rel": "self" + } + ] + } + ] +}`) + }) + + count := 0 + + ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractAPIVersions(page) + if err != nil { + t.Errorf("Failed to extract API versions: %v", err) + return false, err + } + + expected := []APIVersion{ + APIVersion{ + Status: "CURRENT", + ID: "v2.0", + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestNonJSONCannotBeExtractedIntoAPIVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + if _, err := ExtractAPIVersions(page); err == nil { + t.Fatalf("Expected error, got nil") + } + return true, nil + }) +} + +func TestAPIInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.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, ` +{ + "resources": [ + { + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0/subnets", + "rel": "self" + } + ], + "name": "subnet", + "collection": "subnets" + }, + { + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0/networks", + "rel": "self" + } + ], + "name": "network", + "collection": "networks" + }, + { + "links": [ + { + "href": "http://23.253.228.211:9696/v2.0/ports", + "rel": "self" + } + ], + "name": "port", + "collection": "ports" + } + ] +} + `) + }) + + count := 0 + + ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVersionResources(page) + if err != nil { + t.Errorf("Failed to extract version resources: %v", err) + return false, err + } + + expected := []APIVersionResource{ + APIVersionResource{ + Name: "subnet", + Collection: "subnets", + }, + APIVersionResource{ + Name: "network", + Collection: "networks", + }, + APIVersionResource{ + Name: "port", + Collection: "ports", + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestNonJSONCannotBeExtractedIntoAPIVersionResources(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) { + if _, err := ExtractVersionResources(page); err == nil { + t.Fatalf("Expected error, got nil") + } + return true, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/results.go new file mode 100644 index 000000000000..97159341ffbd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/results.go @@ -0,0 +1,77 @@ +package apiversions + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud/pagination" +) + +// APIVersion represents an API version for Neutron. It contains the status of +// the API, and its unique ID. +type APIVersion struct { + Status string `mapstructure:"status" json:"status"` + ID string `mapstructure:"id" json:"id"` +} + +// APIVersionPage is the page returned by a pager when traversing over a +// collection of API versions. +type APIVersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an APIVersionPage struct is empty. +func (r APIVersionPage) IsEmpty() (bool, error) { + is, err := ExtractAPIVersions(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractAPIVersions takes a collection page, extracts all of the elements, +// and returns them a slice of APIVersion structs. It is effectively a cast. +func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) { + var resp struct { + Versions []APIVersion `mapstructure:"versions"` + } + + err := mapstructure.Decode(page.(APIVersionPage).Body, &resp) + + return resp.Versions, err +} + +// APIVersionResource represents a generic API resource. It contains the name +// of the resource and its plural collection name. +type APIVersionResource struct { + Name string `mapstructure:"name" json:"name"` + Collection string `mapstructure:"collection" json:"collection"` +} + +// APIVersionResourcePage is a concrete type which embeds the common +// SinglePageBase struct, and is used when traversing API versions collections. +type APIVersionResourcePage struct { + pagination.SinglePageBase +} + +// IsEmpty is a concrete function which indicates whether an +// APIVersionResourcePage is empty or not. +func (r APIVersionResourcePage) IsEmpty() (bool, error) { + is, err := ExtractVersionResources(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractVersionResources accepts a Page struct, specifically a +// APIVersionResourcePage struct, and extracts the elements into a slice of +// APIVersionResource structs. In other words, the collection is mapped into +// a relevant slice. +func ExtractVersionResources(page pagination.Page) ([]APIVersionResource, error) { + var resp struct { + APIVersionResources []APIVersionResource `mapstructure:"resources"` + } + + err := mapstructure.Decode(page.(APIVersionResourcePage).Body, &resp) + + return resp.APIVersionResources, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls.go new file mode 100644 index 000000000000..58aa2b61f8bd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls.go @@ -0,0 +1,15 @@ +package apiversions + +import ( + "strings" + + "github.com/rackspace/gophercloud" +) + +func apiVersionsURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} + +func apiInfoURL(c *gophercloud.ServiceClient, version string) string { + return c.Endpoint + strings.TrimRight(version, "/") + "/" +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls_test.go new file mode 100644 index 000000000000..7dd069c94f51 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/apiversions/urls_test.go @@ -0,0 +1,26 @@ +package apiversions + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestAPIVersionsURL(t *testing.T) { + actual := apiVersionsURL(endpointClient()) + expected := endpoint + th.AssertEquals(t, expected, actual) +} + +func TestAPIInfoURL(t *testing.T) { + actual := apiInfoURL(endpointClient(), "v2.0") + expected := endpoint + "v2.0/" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/common/common_tests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/common/common_tests.go new file mode 100644 index 000000000000..41603510d62a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/common/common_tests.go @@ -0,0 +1,14 @@ +package common + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper/client" +) + +const TokenID = client.TokenID + +func ServiceClient() *gophercloud.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate.go new file mode 100644 index 000000000000..d08e1fda9773 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate.go @@ -0,0 +1,41 @@ +package extensions + +import ( + "github.com/rackspace/gophercloud" + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" +) + +// Extension is a single OpenStack extension. +type Extension struct { + common.Extension +} + +// GetResult wraps a GetResult from common. +type GetResult struct { + common.GetResult +} + +// ExtractExtensions interprets a Page as a slice of Extensions. +func ExtractExtensions(page pagination.Page) ([]Extension, error) { + inner, err := common.ExtractExtensions(page) + if err != nil { + return nil, err + } + outer := make([]Extension, len(inner)) + for index, ext := range inner { + outer[index] = Extension{ext} + } + return outer, nil +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) GetResult { + return GetResult{common.Get(c, alias)} +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate_test.go new file mode 100644 index 000000000000..3d2ac78d4825 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/delegate_test.go @@ -0,0 +1,105 @@ +package extensions + +import ( + "fmt" + "net/http" + "testing" + + common "github.com/rackspace/gophercloud/openstack/common/extensions" + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/extensions", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + + fmt.Fprintf(w, ` +{ + "extensions": [ + { + "updated": "2013-01-20T00:00:00-00:00", + "name": "Neutron Service Type Management", + "links": [], + "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + "alias": "service-type", + "description": "API for retrieving service providers for Neutron advanced services" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + if err != nil { + t.Errorf("Failed to extract extensions: %v", err) + } + + expected := []Extension{ + Extension{ + common.Extension{ + Updated: "2013-01-20T00:00:00-00:00", + Name: "Neutron Service Type Management", + Links: []interface{}{}, + Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0", + Alias: "service-type", + Description: "API for retrieving service providers for Neutron advanced services", + }, + }, + } + + th.AssertDeepEquals(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("/v2.0/extensions/agent", 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, ` +{ + "extension": { + "updated": "2013-02-03T10:00:00-00:00", + "name": "agent", + "links": [], + "namespace": "http://docs.openstack.org/ext/agent/api/v2.0", + "alias": "agent", + "description": "The agent management extension." + } +} + `) + }) + + ext, err := Get(fake.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00") + th.AssertEquals(t, ext.Name, "agent") + th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0") + th.AssertEquals(t, ext.Alias, "agent") + th.AssertEquals(t, ext.Description, "The agent management extension.") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/doc.go new file mode 100644 index 000000000000..dad3a844f752 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/doc.go @@ -0,0 +1,3 @@ +// Package external provides information and interaction with the external +// extension for the OpenStack Networking service. +package external diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/requests.go new file mode 100644 index 000000000000..097ae37f2437 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/requests.go @@ -0,0 +1,69 @@ +package external + +import ( + "time" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +// CreateOpts is the structure used when creating new external network +// resources. It embeds networks.CreateOpts and so inherits all of its required +// and optional fields, with the addition of the External field. +type CreateOpts struct { + Parent networks.CreateOpts + External bool +} + +// ToNetworkCreateMap casts a CreateOpts struct to a map. +func (o CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + + // DO NOT REMOVE. Though this line seemingly does nothing of value, it is a + // splint to prevent the unit test from failing on Go Tip. We suspect it is a + // compiler issue that will hopefully be worked out prior to our next release. + // Again, for all the unit tests to pass, this line is necessary and sufficient + // at the moment. We should reassess after the Go 1.5 release to determine + // if this line is still needed. + time.Sleep(0 * time.Millisecond) + + outer, err := o.Parent.ToNetworkCreateMap() + if err != nil { + return nil, err + } + + outer["network"].(map[string]interface{})["router:external"] = o.External + + return outer, nil +} + +// UpdateOpts is the structure used when updating existing external network +// resources. It embeds networks.UpdateOpts and so inherits all of its required +// and optional fields, with the addition of the External field. +type UpdateOpts struct { + Parent networks.UpdateOpts + External bool +} + +// ToNetworkUpdateMap casts an UpdateOpts struct to a map. +func (o UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) { + outer, err := o.Parent.ToNetworkUpdateMap() + if err != nil { + return nil, err + } + + outer["network"].(map[string]interface{})["router:external"] = o.External + + return outer, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results.go new file mode 100644 index 000000000000..54dbf4bb69e3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results.go @@ -0,0 +1,81 @@ +package external + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" +) + +// NetworkExternal represents a decorated form of a Network with based on the +// "external-net" extension. +type NetworkExternal struct { + // UUID for the network + ID string `mapstructure:"id" json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `mapstructure:"name" json:"name"` + + // The administrative state of network. If false (down), the network does not forward packets. + AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `mapstructure:"status" json:"status"` + + // Subnets associated with this network. + Subnets []string `mapstructure:"subnets" json:"subnets"` + + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` + + // Specifies whether the network resource can be accessed by any tenant or not. + Shared bool `mapstructure:"shared" json:"shared"` + + // Specifies whether the network is an external network or not. + External bool `mapstructure:"router:external" json:"router:external"` +} + +func commonExtract(e error, response interface{}) (*NetworkExternal, error) { + if e != nil { + return nil, e + } + + var res struct { + Network *NetworkExternal `json:"network"` + } + + err := mapstructure.Decode(response, &res) + + return res.Network, err +} + +// ExtractGet decorates a GetResult struct returned from a networks.Get() +// function with extended attributes. +func ExtractGet(r networks.GetResult) (*NetworkExternal, error) { + return commonExtract(r.Err, r.Body) +} + +// ExtractCreate decorates a CreateResult struct returned from a networks.Create() +// function with extended attributes. +func ExtractCreate(r networks.CreateResult) (*NetworkExternal, error) { + return commonExtract(r.Err, r.Body) +} + +// ExtractUpdate decorates a UpdateResult struct returned from a +// networks.Update() function with extended attributes. +func ExtractUpdate(r networks.UpdateResult) (*NetworkExternal, error) { + return commonExtract(r.Err, r.Body) +} + +// ExtractList accepts a Page struct, specifically a NetworkPage struct, and +// extracts the elements into a slice of NetworkExternal structs. In other +// words, a generic collection is mapped into a relevant slice. +func ExtractList(page pagination.Page) ([]NetworkExternal, error) { + var resp struct { + Networks []NetworkExternal `mapstructure:"networks" json:"networks"` + } + + err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp) + + return resp.Networks, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results_test.go new file mode 100644 index 000000000000..916cd2cfd031 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external/results_test.go @@ -0,0 +1,254 @@ +package external + +import ( + "errors" + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "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() + + 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": [ + { + "admin_state_up": true, + "id": "0f38d5ad-10a6-428f-a5fc-825cfe0f1970", + "name": "net1", + "router:external": false, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "25778974-48a8-46e7-8998-9dc8c70d2f06" + ], + "tenant_id": "b575417a6c444a6eb5cc3a58eb4f714a" + }, + { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "ext_net", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } + ] +} + `) + }) + + count := 0 + + networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractList(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []NetworkExternal{ + NetworkExternal{ + Status: "ACTIVE", + Subnets: []string{"25778974-48a8-46e7-8998-9dc8c70d2f06"}, + Name: "net1", + AdminStateUp: true, + TenantID: "b575417a6c444a6eb5cc3a58eb4f714a", + Shared: false, + ID: "0f38d5ad-10a6-428f-a5fc-825cfe0f1970", + External: false, + }, + NetworkExternal{ + Status: "ACTIVE", + Subnets: []string{"2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"}, + Name: "ext_net", + AdminStateUp: true, + TenantID: "5eb8995cf717462c9df8d1edfa498010", + Shared: false, + ID: "8d05a1b1-297a-46ca-8974-17debf51ca3c", + External: true, + }, + } + + 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": { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "ext_net", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } +} + `) + }) + + res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + n, err := ExtractGet(res) + + th.AssertNoErr(t, err) + th.AssertEquals(t, true, n.External) +} + +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": { + "admin_state_up": true, + "name": "ext_net", + "router:external": true + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "ext_net", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } +} + `) + }) + + options := CreateOpts{networks.CreateOpts{Name: "ext_net", AdminStateUp: Up}, true} + res := networks.Create(fake.ServiceClient(), options) + + n, err := ExtractCreate(res) + + th.AssertNoErr(t, err) + th.AssertEquals(t, true, n.External) +} + +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": { + "router:external": true, + "name": "new_name" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "network": { + "admin_state_up": true, + "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c", + "name": "new_name", + "router:external": true, + "shared": false, + "status": "ACTIVE", + "subnets": [ + "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5" + ], + "tenant_id": "5eb8995cf717462c9df8d1edfa498010" + } +} + `) + }) + + options := UpdateOpts{networks.UpdateOpts{Name: "new_name"}, true} + res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options) + n, err := ExtractUpdate(res) + + th.AssertNoErr(t, err) + th.AssertEquals(t, true, n.External) +} + +func TestExtractFnsReturnsErrWhenResultContainsErr(t *testing.T) { + gr := networks.GetResult{} + gr.Err = errors.New("") + + if _, err := ExtractGet(gr); err == nil { + t.Fatalf("Expected error, got one") + } + + ur := networks.UpdateResult{} + ur.Err = errors.New("") + + if _, err := ExtractUpdate(ur); err == nil { + t.Fatalf("Expected error, got one") + } + + cr := networks.CreateResult{} + cr.Err = errors.New("") + + if _, err := ExtractCreate(cr); err == nil { + t.Fatalf("Expected error, got one") + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/doc.go new file mode 100644 index 000000000000..3ec450a7b3db --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/doc.go @@ -0,0 +1,3 @@ +// Package fwaas provides information and interaction with the Firewall +// as a Service extension for the OpenStack Networking service. +package fwaas diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/errors.go new file mode 100644 index 000000000000..dd92bb20dbe4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/errors.go @@ -0,0 +1,11 @@ +package firewalls + +import "fmt" + +func err(str string) error { + return fmt.Errorf("%s", str) +} + +var ( + errPolicyRequired = err("A policy ID is required") +) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests.go new file mode 100644 index 000000000000..12d587f389b3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests.go @@ -0,0 +1,216 @@ +package firewalls + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Shared gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Yes` and `No` enums. +type Shared *bool + +// Convenience vars for AdminStateUp and Shared values. +var ( + iTrue = true + iFalse = false + Up AdminState = &iTrue + Down AdminState = &iFalse + Yes Shared = &iTrue + No Shared = &iFalse +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFirewallListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the firewall attributes you want to see returned. SortKey allows you to sort +// by a particular firewall attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + TenantID string `q:"tenant_id"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp bool `q:"admin_state_up"` + Shared bool `q:"shared"` + PolicyID string `q:"firewall_policy_id"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToFirewallListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFirewallListQuery() (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 +// firewalls. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +// +// Default policy settings return only those firewalls that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + + if opts != nil { + query, err := opts.ToFirewallListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return FirewallPage{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 { + ToFirewallCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new firewall. +type CreateOpts struct { + // Only required if the caller has an admin role and wants to create a firewall + // for another tenant. + TenantID string + Name string + Description string + AdminStateUp *bool + Shared *bool + PolicyID string +} + +// ToFirewallCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToFirewallCreateMap() (map[string]interface{}, error) { + if opts.PolicyID == "" { + return nil, errPolicyRequired + } + + f := make(map[string]interface{}) + + if opts.TenantID != "" { + f["tenant_id"] = opts.TenantID + } + if opts.Name != "" { + f["name"] = opts.Name + } + if opts.Description != "" { + f["description"] = opts.Description + } + if opts.Shared != nil { + f["shared"] = *opts.Shared + } + if opts.AdminStateUp != nil { + f["admin_state_up"] = *opts.AdminStateUp + } + if opts.PolicyID != "" { + f["firewall_policy_id"] = opts.PolicyID + } + + return map[string]interface{}{"firewall": f}, nil +} + +// Create accepts a CreateOpts struct and uses the values to create a new firewall +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToFirewallCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular firewall based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update 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 UpdateOptsBuilder interface { + ToFirewallUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a firewall. +type UpdateOpts struct { + // Name of the firewall. + Name string + Description string + AdminStateUp *bool + Shared *bool + PolicyID string +} + +// ToFirewallUpdateMap casts a CreateOpts struct to a map. +func (opts UpdateOpts) ToFirewallUpdateMap() (map[string]interface{}, error) { + f := make(map[string]interface{}) + + if opts.Name != "" { + f["name"] = opts.Name + } + if opts.Description != "" { + f["description"] = opts.Description + } + if opts.Shared != nil { + f["shared"] = *opts.Shared + } + if opts.AdminStateUp != nil { + f["admin_state_up"] = *opts.AdminStateUp + } + if opts.PolicyID != "" { + f["firewall_policy_id"] = opts.PolicyID + } + + return map[string]interface{}{"firewall": f}, nil +} + +// Update allows firewalls to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToFirewallUpdateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Delete will permanently delete a particular firewall based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go new file mode 100644 index 000000000000..f24e2835ea9c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go @@ -0,0 +1,246 @@ +package firewalls + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/fw/firewalls", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls", 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, ` +{ + "firewalls":[ + { + "status": "ACTIVE", + "name": "fw1", + "admin_state_up": false, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e28a", + "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", + "description": "OpenStack firewall 1" + }, + { + "status": "PENDING_UPDATE", + "name": "fw2", + "admin_state_up": true, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e299", + "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f99", + "description": "OpenStack firewall 2" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractFirewalls(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + expected := []Firewall{ + Firewall{ + Status: "ACTIVE", + Name: "fw1", + AdminStateUp: false, + TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b", + PolicyID: "34be8c83-4d42-4dca-a74e-b77fffb8e28a", + ID: "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", + Description: "OpenStack firewall 1", + }, + Firewall{ + Status: "PENDING_UPDATE", + Name: "fw2", + AdminStateUp: true, + TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b", + PolicyID: "34be8c83-4d42-4dca-a74e-b77fffb8e299", + ID: "fb5b5315-64f6-4ea3-8e58-981cc37c6f99", + Description: "OpenStack firewall 2", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls", 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, ` +{ + "firewall":{ + "name": "fw", + "description": "OpenStack firewall", + "admin_state_up": true, + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "firewall":{ + "status": "PENDING_CREATE", + "name": "fw", + "description": "OpenStack firewall", + "admin_state_up": true, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c" + } +} + `) + }) + + options := CreateOpts{ + TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b", + Name: "fw", + Description: "OpenStack firewall", + AdminStateUp: Up, + PolicyID: "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + } + _, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls/fb5b5315-64f6-4ea3-8e58-981cc37c6f61", 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, ` +{ + "firewall": { + "status": "ACTIVE", + "name": "fw", + "admin_state_up": true, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e28a", + "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", + "description": "OpenStack firewall" + } +} + `) + }) + + fw, err := Get(fake.ServiceClient(), "fb5b5315-64f6-4ea3-8e58-981cc37c6f61").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "ACTIVE", fw.Status) + th.AssertEquals(t, "fw", fw.Name) + th.AssertEquals(t, "OpenStack firewall", fw.Description) + th.AssertEquals(t, true, fw.AdminStateUp) + th.AssertEquals(t, "34be8c83-4d42-4dca-a74e-b77fffb8e28a", fw.PolicyID) + th.AssertEquals(t, "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", fw.ID) + th.AssertEquals(t, "b4eedccc6fb74fa8a7ad6b08382b852b", fw.TenantID) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls/ea5b5315-64f6-4ea3-8e58-981cc37c6576", 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, ` +{ + "firewall":{ + "name": "fw", + "description": "updated fw", + "admin_state_up":false, + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall": { + "status": "ACTIVE", + "name": "fw", + "admin_state_up": false, + "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c" + "id": "ea5b5315-64f6-4ea3-8e58-981cc37c6576", + "description": "OpenStack firewall", + } +} + `) + }) + + options := UpdateOpts{ + Name: "fw", + Description: "updated fw", + AdminStateUp: Down, + PolicyID: "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c", + } + + _, err := Update(fake.ServiceClient(), "ea5b5315-64f6-4ea3-8e58-981cc37c6576", options).Extract() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewalls/4ec89087-d057-4e2c-911f-60a3b47ee304", 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(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/results.go new file mode 100644 index 000000000000..a8c76eef2322 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/results.go @@ -0,0 +1,101 @@ +package firewalls + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type Firewall struct { + ID string `json:"id" mapstructure:"id"` + Name string `json:"name" mapstructure:"name"` + Description string `json:"description" mapstructure:"description"` + AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + Status string `json:"status" mapstructure:"status"` + PolicyID string `json:"firewall_policy_id" mapstructure:"firewall_policy_id"` + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a firewall. +func (r commonResult) Extract() (*Firewall, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Firewall *Firewall `json:"firewall"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Firewall, err +} + +// FirewallPage is the page returned by a pager when traversing over a +// collection of firewalls. +type FirewallPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of firewalls has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p FirewallPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"firewalls_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a FirewallPage struct is empty. +func (p FirewallPage) IsEmpty() (bool, error) { + is, err := ExtractFirewalls(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractFirewalls accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractFirewalls(page pagination.Page) ([]Firewall, error) { + var resp struct { + Firewalls []Firewall `mapstructure:"firewalls" json:"firewalls"` + } + + err := mapstructure.Decode(page.(FirewallPage).Body, &resp) + + return resp.Firewalls, err +} + +// 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 { + gophercloud.ErrResult +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/urls.go new file mode 100644 index 000000000000..4dde53005a2c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls/urls.go @@ -0,0 +1,16 @@ +package firewalls + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "fw" + resourcePath = "firewalls" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/requests.go new file mode 100644 index 000000000000..fe07d9abb149 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/requests.go @@ -0,0 +1,243 @@ +package policies + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Binary gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Yes` and `No` enums +type Binary *bool + +// Convenience vars for Audited and Shared values. +var ( + iTrue = true + iFalse = false + Yes Binary = &iTrue + No Binary = &iFalse +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPolicyListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the firewall policy attributes you want to see returned. SortKey allows you +// to sort by a particular firewall policy attribute. SortDir sets the direction, +// and is either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + TenantID string `q:"tenant_id"` + Name string `q:"name"` + Description string `q:"description"` + Shared bool `q:"shared"` + Audited bool `q:"audited"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToPolicyListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPolicyListQuery() (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 +// firewall policies. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +// +// Default policy settings return only those firewall policies that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + + if opts != nil { + query, err := opts.ToPolicyListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PolicyPage{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 { + ToPolicyCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new firewall policy. +type CreateOpts struct { + // Only required if the caller has an admin role and wants to create a firewall policy + // for another tenant. + TenantID string + Name string + Description string + Shared *bool + Audited *bool + Rules []string +} + +// ToPolicyCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToPolicyCreateMap() (map[string]interface{}, error) { + p := make(map[string]interface{}) + + if opts.TenantID != "" { + p["tenant_id"] = opts.TenantID + } + if opts.Name != "" { + p["name"] = opts.Name + } + if opts.Description != "" { + p["description"] = opts.Description + } + if opts.Shared != nil { + p["shared"] = *opts.Shared + } + if opts.Audited != nil { + p["audited"] = *opts.Audited + } + if opts.Rules != nil { + p["firewall_rules"] = opts.Rules + } + + return map[string]interface{}{"firewall_policy": p}, nil +} + +// Create accepts a CreateOpts struct and uses the values to create a new firewall policy +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToPolicyCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular firewall policy based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update 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 UpdateOptsBuilder interface { + ToPolicyUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a firewall policy. +type UpdateOpts struct { + // Name of the firewall policy. + Name string + Description string + Shared *bool + Audited *bool + Rules []string +} + +// ToPolicyUpdateMap casts a CreateOpts struct to a map. +func (opts UpdateOpts) ToPolicyUpdateMap() (map[string]interface{}, error) { + p := make(map[string]interface{}) + + if opts.Name != "" { + p["name"] = opts.Name + } + if opts.Description != "" { + p["description"] = opts.Description + } + if opts.Shared != nil { + p["shared"] = *opts.Shared + } + if opts.Audited != nil { + p["audited"] = *opts.Audited + } + if opts.Rules != nil { + p["firewall_rules"] = opts.Rules + } + + return map[string]interface{}{"firewall_policy": p}, nil +} + +// Update allows firewall policies to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToPolicyUpdateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Delete will permanently delete a particular firewall policy based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} + +func InsertRule(c *gophercloud.ServiceClient, policyID, ruleID, beforeID, afterID string) error { + type request struct { + RuleId string `json:"firewall_rule_id"` + Before string `json:"insert_before,omitempty"` + After string `json:"insert_after,omitempty"` + } + + reqBody := request{ + RuleId: ruleID, + Before: beforeID, + After: afterID, + } + + // Send request to API + var res commonResult + _, res.Err = c.Put(insertURL(c, policyID), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res.Err +} + +func RemoveRule(c *gophercloud.ServiceClient, policyID, ruleID string) error { + type request struct { + RuleId string `json:"firewall_rule_id"` + } + + reqBody := request{ + RuleId: ruleID, + } + + // Send request to API + var res commonResult + _, res.Err = c.Put(removeURL(c, policyID), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res.Err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/requests_test.go new file mode 100644 index 000000000000..b9d78652c320 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/requests_test.go @@ -0,0 +1,279 @@ +package policies + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/fw/firewall_policies", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_policies", 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, ` +{ + "firewall_policies": [ + { + "name": "policy1", + "firewall_rules": [ + "75452b36-268e-4e75-aaf4-f0e7ed50bc97", + "c9e77ca0-1bc8-497d-904d-948107873dc6" + ], + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": true, + "shared": false, + "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", + "description": "Firewall policy 1" + }, + { + "name": "policy2", + "firewall_rules": [ + "03d2a6ad-633f-431a-8463-4370d06a22c8" + ], + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": false, + "shared": true, + "id": "c854fab5-bdaf-4a86-9359-78de93e5df01", + "description": "Firewall policy 2" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractPolicies(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + expected := []Policy{ + Policy{ + Name: "policy1", + Rules: []string{ + "75452b36-268e-4e75-aaf4-f0e7ed50bc97", + "c9e77ca0-1bc8-497d-904d-948107873dc6", + }, + TenantID: "9145d91459d248b1b02fdaca97c6a75d", + Audited: true, + Shared: false, + ID: "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", + Description: "Firewall policy 1", + }, + Policy{ + Name: "policy2", + Rules: []string{ + "03d2a6ad-633f-431a-8463-4370d06a22c8", + }, + TenantID: "9145d91459d248b1b02fdaca97c6a75d", + Audited: false, + Shared: true, + ID: "c854fab5-bdaf-4a86-9359-78de93e5df01", + Description: "Firewall policy 2", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_policies", 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, ` +{ + "firewall_policy":{ + "name": "policy", + "firewall_rules": [ + "98a58c87-76be-ae7c-a74e-b77fffb88d95", + "11a58c87-76be-ae7c-a74e-b77fffb88a32" + ], + "description": "Firewall policy", + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": true, + "shared": false + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "firewall_policy":{ + "name": "policy", + "firewall_rules": [ + "98a58c87-76be-ae7c-a74e-b77fffb88d95", + "11a58c87-76be-ae7c-a74e-b77fffb88a32" + ], + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": false, + "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", + "description": "Firewall policy" + } +} + `) + }) + + options := CreateOpts{ + TenantID: "9145d91459d248b1b02fdaca97c6a75d", + Name: "policy", + Description: "Firewall policy", + Shared: No, + Audited: Yes, + Rules: []string{ + "98a58c87-76be-ae7c-a74e-b77fffb88d95", + "11a58c87-76be-ae7c-a74e-b77fffb88a32", + }, + } + + _, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_policies/bcab5315-64f6-4ea3-8e58-981cc37c6f61", 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, ` +{ + "firewall_policy":{ + "name": "www", + "firewall_rules": [ + "75452b36-268e-4e75-aaf4-f0e7ed50bc97", + "c9e77ca0-1bc8-497d-904d-948107873dc6", + "03d2a6ad-633f-431a-8463-4370d06a22c8" + ], + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": false, + "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", + "description": "Firewall policy web" + } +} + `) + }) + + policy, err := Get(fake.ServiceClient(), "bcab5315-64f6-4ea3-8e58-981cc37c6f61").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "www", policy.Name) + th.AssertEquals(t, "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", policy.ID) + th.AssertEquals(t, "Firewall policy web", policy.Description) + th.AssertEquals(t, 3, len(policy.Rules)) + th.AssertEquals(t, "75452b36-268e-4e75-aaf4-f0e7ed50bc97", policy.Rules[0]) + th.AssertEquals(t, "c9e77ca0-1bc8-497d-904d-948107873dc6", policy.Rules[1]) + th.AssertEquals(t, "03d2a6ad-633f-431a-8463-4370d06a22c8", policy.Rules[2]) + th.AssertEquals(t, "9145d91459d248b1b02fdaca97c6a75d", policy.TenantID) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_policies/f2b08c1e-aa81-4668-8ae1-1401bcb0576c", 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, ` +{ + "firewall_policy":{ + "name": "policy", + "firewall_rules": [ + "98a58c87-76be-ae7c-a74e-b77fffb88d95", + "11a58c87-76be-ae7c-a74e-b77fffb88a32" + ], + "description": "Firewall policy" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall_policy":{ + "name": "policy", + "firewall_rules": [ + "75452b36-268e-4e75-aaf4-f0e7ed50bc97", + "c9e77ca0-1bc8-497d-904d-948107873dc6", + "03d2a6ad-633f-431a-8463-4370d06a22c8" + ], + "tenant_id": "9145d91459d248b1b02fdaca97c6a75d", + "audited": false, + "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", + "description": "Firewall policy" + } +} + `) + }) + + options := UpdateOpts{ + Name: "policy", + Description: "Firewall policy", + Rules: []string{ + "98a58c87-76be-ae7c-a74e-b77fffb88d95", + "11a58c87-76be-ae7c-a74e-b77fffb88a32", + }, + } + + _, err := Update(fake.ServiceClient(), "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", options).Extract() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_policies/4ec89077-d057-4a2b-911f-60a3b47ee304", 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(), "4ec89077-d057-4a2b-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/results.go new file mode 100644 index 000000000000..a9a0c358d575 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/results.go @@ -0,0 +1,101 @@ +package policies + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type Policy struct { + ID string `json:"id" mapstructure:"id"` + Name string `json:"name" mapstructure:"name"` + Description string `json:"description" mapstructure:"description"` + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` + Audited bool `json:"audited" mapstructure:"audited"` + Shared bool `json:"shared" mapstructure:"shared"` + Rules []string `json:"firewall_rules,omitempty" mapstructure:"firewall_rules"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a firewall policy. +func (r commonResult) Extract() (*Policy, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Policy *Policy `json:"firewall_policy" mapstructure:"firewall_policy"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Policy, err +} + +// PolicyPage is the page returned by a pager when traversing over a +// collection of firewall policies. +type PolicyPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of firewall policies has +// reached the end of a page and the pager seeks to traverse over a new one. +// In order to do this, it needs to construct the next page's URL. +func (p PolicyPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"firewall_policies_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a PolicyPage struct is empty. +func (p PolicyPage) IsEmpty() (bool, error) { + is, err := ExtractPolicies(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractPolicies accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPolicies(page pagination.Page) ([]Policy, error) { + var resp struct { + Policies []Policy `mapstructure:"firewall_policies" json:"firewall_policies"` + } + + err := mapstructure.Decode(page.(PolicyPage).Body, &resp) + + return resp.Policies, err +} + +// 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 { + gophercloud.ErrResult +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/urls.go new file mode 100644 index 000000000000..27ea9ae61437 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies/urls.go @@ -0,0 +1,26 @@ +package policies + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "fw" + resourcePath = "firewall_policies" + insertPath = "insert_rule" + removePath = "remove_rule" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func insertURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, insertPath) +} + +func removeURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, removePath) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/errors.go new file mode 100644 index 000000000000..0b29d39fd9e1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/errors.go @@ -0,0 +1,12 @@ +package rules + +import "fmt" + +func err(str string) error { + return fmt.Errorf("%s", str) +} + +var ( + errProtocolRequired = err("A protocol is required (tcp, udp, icmp or any)") + errActionRequired = err("An action is required (allow or deny)") +) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/requests.go new file mode 100644 index 000000000000..57a0e8baffcc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/requests.go @@ -0,0 +1,285 @@ +package rules + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Binary gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Yes` and `No` enums +type Binary *bool + +// Convenience vars for Enabled and Shared values. +var ( + iTrue = true + iFalse = false + Yes Binary = &iTrue + No Binary = &iFalse +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToRuleListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Firewall rule attributes you want to see returned. SortKey allows you to +// sort by a particular firewall rule attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + TenantID string `q:"tenant_id"` + Name string `q:"name"` + Description string `q:"description"` + Protocol string `q:"protocol"` + Action string `q:"action"` + IPVersion int `q:"ip_version"` + SourceIPAddress string `q:"source_ip_address"` + DestinationIPAddress string `q:"destination_ip_address"` + SourcePort string `q:"source_port"` + DestinationPort string `q:"destination_port"` + Enabled bool `q:"enabled"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToRuleListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRuleListQuery() (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 +// firewall rules. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +// +// Default policy settings return only those firewall rules that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + + if opts != nil { + query, err := opts.ToRuleListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return RulePage{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 { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new firewall rule. +type CreateOpts struct { + // Mandatory for create + Protocol string + Action string + // Optional + TenantID string + Name string + Description string + IPVersion int + SourceIPAddress string + DestinationIPAddress string + SourcePort string + DestinationPort string + Shared *bool + Enabled *bool +} + +// ToRuleCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) { + if opts.Protocol == "" { + return nil, errProtocolRequired + } + + if opts.Action == "" { + return nil, errActionRequired + } + + r := make(map[string]interface{}) + + r["protocol"] = opts.Protocol + r["action"] = opts.Action + + if opts.TenantID != "" { + r["tenant_id"] = opts.TenantID + } + if opts.Name != "" { + r["name"] = opts.Name + } + if opts.Description != "" { + r["description"] = opts.Description + } + if opts.IPVersion != 0 { + r["ip_version"] = opts.IPVersion + } + if opts.SourceIPAddress != "" { + r["source_ip_address"] = opts.SourceIPAddress + } + if opts.DestinationIPAddress != "" { + r["destination_ip_address"] = opts.DestinationIPAddress + } + if opts.SourcePort != "" { + r["source_port"] = opts.SourcePort + } + if opts.DestinationPort != "" { + r["destination_port"] = opts.DestinationPort + } + if opts.Shared != nil { + r["shared"] = *opts.Shared + } + if opts.Enabled != nil { + r["enabled"] = *opts.Enabled + } + + return map[string]interface{}{"firewall_rule": r}, nil +} + +// Create accepts a CreateOpts struct and uses the values to create a new firewall rule +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToRuleCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular firewall rule based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update 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 UpdateOptsBuilder interface { + ToRuleUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a firewall rule. +// Optional +type UpdateOpts struct { + Protocol string + Action string + Name string + Description string + IPVersion int + SourceIPAddress *string + DestinationIPAddress *string + SourcePort *string + DestinationPort *string + Shared *bool + Enabled *bool +} + +// ToRuleUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToRuleUpdateMap() (map[string]interface{}, error) { + r := make(map[string]interface{}) + + if opts.Protocol != "" { + r["protocol"] = opts.Protocol + } + if opts.Action != "" { + r["action"] = opts.Action + } + if opts.Name != "" { + r["name"] = opts.Name + } + if opts.Description != "" { + r["description"] = opts.Description + } + if opts.IPVersion != 0 { + r["ip_version"] = opts.IPVersion + } + if opts.SourceIPAddress != nil { + s := *opts.SourceIPAddress + if s == "" { + r["source_ip_address"] = nil + } else { + r["source_ip_address"] = s + } + } + if opts.DestinationIPAddress != nil { + s := *opts.DestinationIPAddress + if s == "" { + r["destination_ip_address"] = nil + } else { + r["destination_ip_address"] = s + } + } + if opts.SourcePort != nil { + s := *opts.SourcePort + if s == "" { + r["source_port"] = nil + } else { + r["source_port"] = s + } + } + if opts.DestinationPort != nil { + s := *opts.DestinationPort + if s == "" { + r["destination_port"] = nil + } else { + r["destination_port"] = s + } + } + if opts.Shared != nil { + r["shared"] = *opts.Shared + } + if opts.Enabled != nil { + r["enabled"] = *opts.Enabled + } + + return map[string]interface{}{"firewall_rule": r}, nil +} + +// Update allows firewall policies to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToRuleUpdateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return res +} + +// Delete will permanently delete a particular firewall rule based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/requests_test.go new file mode 100644 index 000000000000..36f89fa5c49e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/requests_test.go @@ -0,0 +1,328 @@ +package rules + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/fw/firewall_rules", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules", 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, ` +{ + "firewall_rules": [ + { + "protocol": "tcp", + "description": "ssh rule", + "source_port": null, + "source_ip_address": null, + "destination_ip_address": "192.168.1.0/24", + "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919", + "position": 2, + "destination_port": "22", + "id": "f03bd950-6c56-4f5e-a307-45967078f507", + "name": "ssh_form_any", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": true, + "action": "allow", + "ip_version": 4, + "shared": false + }, + { + "protocol": "udp", + "description": "udp rule", + "source_port": null, + "source_ip_address": null, + "destination_ip_address": null, + "firewall_policy_id": "98d7fb51-698c-4123-87e8-f1eee6b5ab7e", + "position": 1, + "destination_port": null, + "id": "ab7bd950-6c56-4f5e-a307-45967078f890", + "name": "deny_all_udp", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": true, + "action": "deny", + "ip_version": 4, + "shared": false + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + expected := []Rule{ + Rule{ + Protocol: "tcp", + Description: "ssh rule", + SourcePort: "", + SourceIPAddress: "", + DestinationIPAddress: "192.168.1.0/24", + PolicyID: "e2a5fb51-698c-4898-87e8-f1eee6b50919", + Position: 2, + DestinationPort: "22", + ID: "f03bd950-6c56-4f5e-a307-45967078f507", + Name: "ssh_form_any", + TenantID: "80cf934d6ffb4ef5b244f1c512ad1e61", + Enabled: true, + Action: "allow", + IPVersion: 4, + Shared: false, + }, + Rule{ + Protocol: "udp", + Description: "udp rule", + SourcePort: "", + SourceIPAddress: "", + DestinationIPAddress: "", + PolicyID: "98d7fb51-698c-4123-87e8-f1eee6b5ab7e", + Position: 1, + DestinationPort: "", + ID: "ab7bd950-6c56-4f5e-a307-45967078f890", + Name: "deny_all_udp", + TenantID: "80cf934d6ffb4ef5b244f1c512ad1e61", + Enabled: true, + Action: "deny", + IPVersion: 4, + Shared: false, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules", 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, ` +{ + "firewall_rule": { + "protocol": "tcp", + "description": "ssh rule", + "destination_ip_address": "192.168.1.0/24", + "destination_port": "22", + "name": "ssh_form_any", + "action": "allow", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "firewall_rule":{ + "protocol": "tcp", + "description": "ssh rule", + "source_port": null, + "source_ip_address": null, + "destination_ip_address": "192.168.1.0/24", + "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919", + "position": 2, + "destination_port": "22", + "id": "f03bd950-6c56-4f5e-a307-45967078f507", + "name": "ssh_form_any", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": true, + "action": "allow", + "ip_version": 4, + "shared": false + } +} + `) + }) + + options := CreateOpts{ + TenantID: "80cf934d6ffb4ef5b244f1c512ad1e61", + Protocol: "tcp", + Description: "ssh rule", + DestinationIPAddress: "192.168.1.0/24", + DestinationPort: "22", + Name: "ssh_form_any", + Action: "allow", + } + + _, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules/f03bd950-6c56-4f5e-a307-45967078f507", 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, ` +{ + "firewall_rule":{ + "protocol": "tcp", + "description": "ssh rule", + "source_port": null, + "source_ip_address": null, + "destination_ip_address": "192.168.1.0/24", + "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919", + "position": 2, + "destination_port": "22", + "id": "f03bd950-6c56-4f5e-a307-45967078f507", + "name": "ssh_form_any", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": true, + "action": "allow", + "ip_version": 4, + "shared": false + } +} + `) + }) + + rule, err := Get(fake.ServiceClient(), "f03bd950-6c56-4f5e-a307-45967078f507").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "tcp", rule.Protocol) + th.AssertEquals(t, "ssh rule", rule.Description) + th.AssertEquals(t, "192.168.1.0/24", rule.DestinationIPAddress) + th.AssertEquals(t, "e2a5fb51-698c-4898-87e8-f1eee6b50919", rule.PolicyID) + th.AssertEquals(t, 2, rule.Position) + th.AssertEquals(t, "22", rule.DestinationPort) + th.AssertEquals(t, "f03bd950-6c56-4f5e-a307-45967078f507", rule.ID) + th.AssertEquals(t, "ssh_form_any", rule.Name) + th.AssertEquals(t, "80cf934d6ffb4ef5b244f1c512ad1e61", rule.TenantID) + th.AssertEquals(t, true, rule.Enabled) + th.AssertEquals(t, "allow", rule.Action) + th.AssertEquals(t, 4, rule.IPVersion) + th.AssertEquals(t, false, rule.Shared) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules/f03bd950-6c56-4f5e-a307-45967078f507", 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, ` +{ + "firewall_rule":{ + "protocol": "tcp", + "description": "ssh rule", + "destination_ip_address": "192.168.1.0/24", + "destination_port": "22", + "source_ip_address": null, + "source_port": null, + "name": "ssh_form_any", + "action": "allow", + "enabled": false + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "firewall_rule":{ + "protocol": "tcp", + "description": "ssh rule", + "source_port": null, + "source_ip_address": null, + "destination_ip_address": "192.168.1.0/24", + "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919", + "position": 2, + "destination_port": "22", + "id": "f03bd950-6c56-4f5e-a307-45967078f507", + "name": "ssh_form_any", + "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61", + "enabled": false, + "action": "allow", + "ip_version": 4, + "shared": false + } +} + `) + }) + + destinationIPAddress := "192.168.1.0/24" + destinationPort := "22" + empty := "" + + options := UpdateOpts{ + Protocol: "tcp", + Description: "ssh rule", + DestinationIPAddress: &destinationIPAddress, + DestinationPort: &destinationPort, + Name: "ssh_form_any", + SourceIPAddress: &empty, + SourcePort: &empty, + Action: "allow", + Enabled: No, + } + + _, err := Update(fake.ServiceClient(), "f03bd950-6c56-4f5e-a307-45967078f507", options).Extract() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/fw/firewall_rules/4ec89077-d057-4a2b-911f-60a3b47ee304", 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(), "4ec89077-d057-4a2b-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/results.go new file mode 100644 index 000000000000..d772024b39fe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/results.go @@ -0,0 +1,110 @@ +package rules + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Rule represents a firewall rule +type Rule struct { + ID string `json:"id" mapstructure:"id"` + Name string `json:"name,omitempty" mapstructure:"name"` + Description string `json:"description,omitempty" mapstructure:"description"` + Protocol string `json:"protocol" mapstructure:"protocol"` + Action string `json:"action" mapstructure:"action"` + IPVersion int `json:"ip_version,omitempty" mapstructure:"ip_version"` + SourceIPAddress string `json:"source_ip_address,omitempty" mapstructure:"source_ip_address"` + DestinationIPAddress string `json:"destination_ip_address,omitempty" mapstructure:"destination_ip_address"` + SourcePort string `json:"source_port,omitempty" mapstructure:"source_port"` + DestinationPort string `json:"destination_port,omitempty" mapstructure:"destination_port"` + Shared bool `json:"shared,omitempty" mapstructure:"shared"` + Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"` + PolicyID string `json:"firewall_policy_id" mapstructure:"firewall_policy_id"` + Position int `json:"position" mapstructure:"position"` + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` +} + +// RulePage is the page returned by a pager when traversing over a +// collection of firewall rules. +type RulePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of firewall rules has +// reached the end of a page and the pager seeks to traverse over a new one. +// In order to do this, it needs to construct the next page's URL. +func (p RulePage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"firewall_rules_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a RulePage struct is empty. +func (p RulePage) IsEmpty() (bool, error) { + is, err := ExtractRules(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractRules accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRules(page pagination.Page) ([]Rule, error) { + var resp struct { + Rules []Rule `mapstructure:"firewall_rules" json:"firewall_rules"` + } + + err := mapstructure.Decode(page.(RulePage).Body, &resp) + + return resp.Rules, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a firewall rule. +func (r commonResult) Extract() (*Rule, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Rule *Rule `json:"firewall_rule" mapstructure:"firewall_rule"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Rule, err +} + +// 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 { + gophercloud.ErrResult +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/urls.go new file mode 100644 index 000000000000..20b08791ed29 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules/urls.go @@ -0,0 +1,16 @@ +package rules + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "fw" + resourcePath = "firewall_rules" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/doc.go new file mode 100644 index 000000000000..d533458267e8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/doc.go @@ -0,0 +1,5 @@ +// Package layer3 provides access to the Layer-3 networking extension for the +// OpenStack Neutron service. This extension allows API users to route packets +// between subnets, forward packets from internal networks to external ones, +// and access instances from external networks through floating IPs. +package layer3 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go new file mode 100644 index 000000000000..49d6f0b7a51c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go @@ -0,0 +1,167 @@ +package floatingips + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + FloatingNetworkID string `q:"floating_network_id"` + PortID string `q:"port_id"` + FixedIP string `q:"fixed_ip_address"` + FloatingIP string `q:"floating_ip_address"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// floating IP resources. It accepts a ListOpts struct, which allows you to +// filter and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return FloatingIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOpts contains all the values needed to create a new floating IP +// resource. The only required fields are FloatingNetworkID and PortID which +// refer to the external network and internal port respectively. +type CreateOpts struct { + FloatingNetworkID string + FloatingIP string + PortID string + FixedIP string + TenantID string +} + +var ( + errFloatingNetworkIDRequired = fmt.Errorf("A NetworkID is required") +) + +// Create accepts a CreateOpts struct and uses the values provided to create a +// new floating IP resource. You can create floating IPs on external networks +// only. If you provide a FloatingNetworkID which refers to a network that is +// not external (i.e. its `router:external' attribute is False), the operation +// will fail and return a 400 error. +// +// If you do not specify a FloatingIP address value, the operation will +// automatically allocate an available address for the new resource. If you do +// choose to specify one, it must fall within the subnet range for the external +// network - otherwise the operation returns a 400 error. If the FloatingIP +// address is already in use, the operation returns a 409 error code. +// +// You can associate the new resource with an internal port by using the PortID +// field. If you specify a PortID that is not valid, the operation will fail and +// return 404 error code. +// +// You must also configure an IP address for the port associated with the PortID +// you have provided - this is what the FixedIP refers to: an IP fixed to a port. +// Because a port might be associated with multiple IP addresses, you can use +// the FixedIP field to associate a particular IP address rather than have the +// API assume for you. If you specify an IP address that is not valid, the +// operation will fail and return a 400 error code. If the PortID and FixedIP +// are already associated with another resource, the operation will fail and +// returns a 409 error code. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate + if opts.FloatingNetworkID == "" { + res.Err = errFloatingNetworkIDRequired + return res + } + + // Define structures + type floatingIP struct { + FloatingNetworkID string `json:"floating_network_id"` + FloatingIP string `json:"floating_ip_address,omitempty"` + PortID string `json:"port_id,omitempty"` + FixedIP string `json:"fixed_ip_address,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + } + type request struct { + FloatingIP floatingIP `json:"floatingip"` + } + + // Populate request body + reqBody := request{FloatingIP: floatingIP{ + FloatingNetworkID: opts.FloatingNetworkID, + PortID: opts.PortID, + FixedIP: opts.FixedIP, + TenantID: opts.TenantID, + }} + + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular floating IP resource based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// UpdateOpts contains the values used when updating a floating IP resource. The +// only value that can be updated is which internal port the floating IP is +// linked to. To associate the floating IP with a new internal port, provide its +// ID. To disassociate the floating IP from all ports, provide an empty string. +type UpdateOpts struct { + PortID string +} + +// Update allows floating IP resources to be updated. Currently, the only way to +// "update" a floating IP is to associate it with a new internal port, or +// disassociated it from all ports. See UpdateOpts for instructions of how to +// do this. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + type floatingIP struct { + PortID *string `json:"port_id"` + } + + type request struct { + FloatingIP floatingIP `json:"floatingip"` + } + + var portID *string + if opts.PortID == "" { + portID = nil + } else { + portID = &opts.PortID + } + + reqBody := request{FloatingIP: floatingIP{PortID: portID}} + + // Send request to API + var res UpdateResult + _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return res +} + +// Delete will permanently delete a particular floating IP resource. Please +// ensure this is what you want - you can also disassociate the IP from existing +// internal ports. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go new file mode 100644 index 000000000000..d914a799bfcf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go @@ -0,0 +1,355 @@ +package floatingips + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", 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, ` +{ + "floatingips": [ + { + "floating_network_id": "6d67c30a-ddb4-49a1-bec3-a65b286b4170", + "router_id": null, + "fixed_ip_address": null, + "floating_ip_address": "192.0.0.4", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "status": "DOWN", + "port_id": null, + "id": "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e" + }, + { + "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64", + "router_id": "0a24cb83-faf5-4d7f-b723-3144ed8a2167", + "fixed_ip_address": "192.0.0.2", + "floating_ip_address": "10.0.0.3", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "status": "DOWN", + "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25", + "id": "ada25a95-f321-4f59-b0e0-f3a970dd3d63" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractFloatingIPs(page) + if err != nil { + t.Errorf("Failed to extract floating IPs: %v", err) + return false, err + } + + expected := []FloatingIP{ + FloatingIP{ + FloatingNetworkID: "6d67c30a-ddb4-49a1-bec3-a65b286b4170", + FixedIP: "", + FloatingIP: "192.0.0.4", + TenantID: "017d8de156df4177889f31a9bd6edc00", + Status: "DOWN", + PortID: "", + ID: "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e", + }, + FloatingIP{ + FloatingNetworkID: "90f742b1-6d17-487b-ba95-71881dbc0b64", + FixedIP: "192.0.0.2", + FloatingIP: "10.0.0.3", + TenantID: "017d8de156df4177889f31a9bd6edc00", + Status: "DOWN", + PortID: "74a342ce-8e07-4e91-880c-9f834b68fa25", + ID: "ada25a95-f321-4f59-b0e0-f3a970dd3d63", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestInvalidNextPageURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"floatingips": [{}], "floatingips_links": {}}`) + }) + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + ExtractFloatingIPs(page) + return true, nil + }) +} + +func TestRequiredFieldsForCreate(t *testing.T) { + res1 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: ""}) + if res1.Err == nil { + t.Fatalf("Expected error, got none") + } + + res2 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: "foo", PortID: ""}) + if res2.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", 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, ` +{ + "floatingip": { + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": "10.0.0.3", + "floating_ip_address": "", + "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + options := CreateOpts{ + FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57", + PortID: "ce705c24-c1ef-408a-bda3-7bbd946164ab", + } + + ip, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID) + th.AssertEquals(t, "4969c491a3c74ee4af974e6d800c62de", ip.TenantID) + th.AssertEquals(t, "376da547-b977-4cfe-9cba-275c80debf57", ip.FloatingNetworkID) + th.AssertEquals(t, "", ip.FloatingIP) + th.AssertEquals(t, "ce705c24-c1ef-408a-bda3-7bbd946164ab", ip.PortID) + th.AssertEquals(t, "10.0.0.3", ip.FixedIP) +} + +func TestCreateEmptyPort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips", 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, ` + { + "floatingip": { + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` + { + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": "10.0.0.3", + "floating_ip_address": "", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } + } + `) + }) + + options := CreateOpts{ + FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57", + } + + ip, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID) + th.AssertEquals(t, "4969c491a3c74ee4af974e6d800c62de", ip.TenantID) + th.AssertEquals(t, "376da547-b977-4cfe-9cba-275c80debf57", ip.FloatingNetworkID) + th.AssertEquals(t, "", ip.FloatingIP) + th.AssertEquals(t, "", ip.PortID) + th.AssertEquals(t, "10.0.0.3", ip.FixedIP) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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, ` +{ + "floatingip": { + "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64", + "fixed_ip_address": "192.0.0.2", + "floating_ip_address": "10.0.0.3", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "status": "DOWN", + "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + ip, err := Get(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "90f742b1-6d17-487b-ba95-71881dbc0b64", ip.FloatingNetworkID) + th.AssertEquals(t, "10.0.0.3", ip.FloatingIP) + th.AssertEquals(t, "74a342ce-8e07-4e91-880c-9f834b68fa25", ip.PortID) + th.AssertEquals(t, "192.0.0.2", ip.FixedIP) + th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", ip.TenantID) + th.AssertEquals(t, "DOWN", ip.Status) + th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID) +} + +func TestAssociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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, ` +{ + "floatingip": { + "port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": null, + "floating_ip_address": "172.24.4.228", + "port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e", + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + ip, err := Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", UpdateOpts{PortID: "423abc8d-2991-4a55-ba98-2aaea84cc72e"}).Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, "423abc8d-2991-4a55-ba98-2aaea84cc72e", ip.PortID) +} + +func TestDisassociate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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, ` +{ + "floatingip": { + "port_id": null + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "floatingip": { + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f", + "tenant_id": "4969c491a3c74ee4af974e6d800c62de", + "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": null, + "floating_ip_address": "172.24.4.228", + "port_id": null, + "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + } +} + `) + }) + + ip, err := Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", UpdateOpts{}).Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, "", ip.FixedIP) + th.AssertDeepEquals(t, "", ip.PortID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go new file mode 100644 index 000000000000..a1c7afe2ceef --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go @@ -0,0 +1,127 @@ +package floatingips + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// FloatingIP represents a floating IP resource. A floating IP is an external +// IP address that is mapped to an internal port and, optionally, a specific +// IP address on a private network. In other words, it enables access to an +// instance on a private network from an external network. For this reason, +// floating IPs can only be defined on networks where the `router:external' +// attribute (provided by the external network extension) is set to True. +type FloatingIP struct { + // Unique identifier for the floating IP instance. + ID string `json:"id" mapstructure:"id"` + + // UUID of the external network where the floating IP is to be created. + FloatingNetworkID string `json:"floating_network_id" mapstructure:"floating_network_id"` + + // Address of the floating IP on the external network. + FloatingIP string `json:"floating_ip_address" mapstructure:"floating_ip_address"` + + // UUID of the port on an internal network that is associated with the floating IP. + PortID string `json:"port_id" mapstructure:"port_id"` + + // The specific IP address of the internal port which should be associated + // with the floating IP. + FixedIP string `json:"fixed_ip_address" mapstructure:"fixed_ip_address"` + + // Owner of the floating IP. Only admin users can specify a tenant identifier + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` + + // The condition of the API resource. + Status string `json:"status" mapstructure:"status"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract a result and extracts a FloatingIP resource. +func (r commonResult) Extract() (*FloatingIP, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + FloatingIP *FloatingIP `json:"floatingip"` + } + + err := mapstructure.Decode(r.Body, &res) + if err != nil { + return nil, fmt.Errorf("Error decoding Neutron floating IP: %v", err) + } + + return res.FloatingIP, nil +} + +// 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 an update operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// FloatingIPPage is the page returned by a pager when traversing over a +// collection of floating IPs. +type FloatingIPPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of floating IPs has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p FloatingIPPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"floatingips_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (p FloatingIPPage) IsEmpty() (bool, error) { + is, err := ExtractFloatingIPs(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractFloatingIPs accepts a Page struct, specifically a FloatingIPPage struct, +// and extracts the elements into a slice of FloatingIP structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractFloatingIPs(page pagination.Page) ([]FloatingIP, error) { + var resp struct { + FloatingIPs []FloatingIP `mapstructure:"floatingips" json:"floatingips"` + } + + err := mapstructure.Decode(page.(FloatingIPPage).Body, &resp) + + return resp.FloatingIPs, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go new file mode 100644 index 000000000000..355f20dc0961 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go @@ -0,0 +1,13 @@ +package floatingips + +import "github.com/rackspace/gophercloud" + +const resourcePath = "floatingips" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go new file mode 100644 index 000000000000..077a71755a8e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests.go @@ -0,0 +1,224 @@ +package routers + +import ( + "errors" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return RouterPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOpts contains all the values needed to create a new router. There are +// no required values. +type CreateOpts struct { + Name string + AdminStateUp *bool + TenantID string + GatewayInfo *GatewayInfo +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// logical router. When it is created, the router does not have an internal +// interface - it is not associated to any subnet. +// +// You can optionally specify an external gateway for a router using the +// GatewayInfo struct. The external gateway for the router must be plugged into +// an external network (it is external if its `router:external' field is set to +// true). +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + type router struct { + Name *string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + TenantID *string `json:"tenant_id,omitempty"` + GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` + } + + type request struct { + Router router `json:"router"` + } + + reqBody := request{Router: router{ + Name: gophercloud.MaybeString(opts.Name), + AdminStateUp: opts.AdminStateUp, + TenantID: gophercloud.MaybeString(opts.TenantID), + }} + + if opts.GatewayInfo != nil { + reqBody.Router.GatewayInfo = opts.GatewayInfo + } + + var res CreateResult + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular router based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// UpdateOpts contains the values used when updating a router. +type UpdateOpts struct { + Name string + AdminStateUp *bool + GatewayInfo *GatewayInfo +} + +// Update allows routers to be updated. You can update the name, administrative +// state, and the external gateway. For more information about how to set the +// external gateway for a router, see Create. This operation does not enable +// the update of router interfaces. To do this, use the AddInterface and +// RemoveInterface functions. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + type router struct { + Name *string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` + } + + type request struct { + Router router `json:"router"` + } + + reqBody := request{Router: router{ + Name: gophercloud.MaybeString(opts.Name), + AdminStateUp: opts.AdminStateUp, + }} + + if opts.GatewayInfo != nil { + reqBody.Router.GatewayInfo = opts.GatewayInfo + } + + // Send request to API + var res UpdateResult + _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return res +} + +// Delete will permanently delete a particular router based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} + +var errInvalidInterfaceOpts = errors.New("When adding a router interface you must provide either a subnet ID or a port ID") + +// InterfaceOpts allow you to work with operations that either add or remote +// an internal interface from a router. +type InterfaceOpts struct { + SubnetID string + PortID string +} + +// AddInterface attaches a subnet to an internal router interface. You must +// specify either a SubnetID or PortID in the request body. If you specify both, +// the operation will fail and an error will be returned. +// +// If you specify a SubnetID, the gateway IP address for that particular subnet +// is used to create the router interface. Alternatively, if you specify a +// PortID, the IP address associated with the port is used to create the router +// interface. +// +// If you reference a port that is associated with multiple IP addresses, or +// if the port is associated with zero IP addresses, the operation will fail and +// a 400 Bad Request error will be returned. +// +// If you reference a port already in use, the operation will fail and a 409 +// Conflict error will be returned. +// +// The PortID that is returned after using Extract() on the result of this +// operation can either be the same PortID passed in or, on the other hand, the +// identifier of a new port created by this operation. After the operation +// completes, the device ID of the port is set to the router ID, and the +// device owner attribute is set to `network:router_interface'. +func AddInterface(c *gophercloud.ServiceClient, id string, opts InterfaceOpts) InterfaceResult { + var res InterfaceResult + + // Validate + if (opts.SubnetID == "" && opts.PortID == "") || (opts.SubnetID != "" && opts.PortID != "") { + res.Err = errInvalidInterfaceOpts + return res + } + + type request struct { + SubnetID string `json:"subnet_id,omitempty"` + PortID string `json:"port_id,omitempty"` + } + + body := request{SubnetID: opts.SubnetID, PortID: opts.PortID} + + _, res.Err = c.Put(addInterfaceURL(c, id), body, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return res +} + +// RemoveInterface removes an internal router interface, which detaches a +// subnet from the router. You must specify either a SubnetID or PortID, since +// these values are used to identify the router interface to remove. +// +// Unlike AddInterface, you can also specify both a SubnetID and PortID. If you +// choose to specify both, the subnet ID must correspond to the subnet ID of +// the first IP address on the port specified by the port ID. Otherwise, the +// operation will fail and return a 409 Conflict error. +// +// If the router, subnet or port which are referenced do not exist or are not +// visible to you, the operation will fail and a 404 Not Found error will be +// returned. After this operation completes, the port connecting the router +// with the subnet is removed from the subnet for the network. +func RemoveInterface(c *gophercloud.ServiceClient, id string, opts InterfaceOpts) InterfaceResult { + var res InterfaceResult + + type request struct { + SubnetID string `json:"subnet_id,omitempty"` + PortID string `json:"port_id,omitempty"` + } + + body := request{SubnetID: opts.SubnetID, PortID: opts.PortID} + + _, res.Err = c.Put(removeInterfaceURL(c, id), body, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests_test.go new file mode 100644 index 000000000000..c34264daee55 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/requests_test.go @@ -0,0 +1,338 @@ +package routers + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/routers", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers", 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, ` +{ + "routers": [ + { + "status": "ACTIVE", + "external_gateway_info": null, + "name": "second_routers", + "admin_state_up": true, + "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3", + "id": "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b" + }, + { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8" + }, + "name": "router1", + "admin_state_up": true, + "tenant_id": "33a40233088643acb66ff6eb0ebea679", + "id": "a9254bdb-2613-4a13-ac4c-adc581fba50d" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractRouters(page) + if err != nil { + t.Errorf("Failed to extract routers: %v", err) + return false, err + } + + expected := []Router{ + Router{ + Status: "ACTIVE", + GatewayInfo: GatewayInfo{NetworkID: ""}, + AdminStateUp: true, + Name: "second_routers", + ID: "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b", + TenantID: "6b96ff0cb17a4b859e1e575d221683d3", + }, + Router{ + Status: "ACTIVE", + GatewayInfo: GatewayInfo{NetworkID: "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"}, + AdminStateUp: true, + Name: "router1", + ID: "a9254bdb-2613-4a13-ac4c-adc581fba50d", + TenantID: "33a40233088643acb66ff6eb0ebea679", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers", 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, ` +{ + "router":{ + "name": "foo_router", + "admin_state_up": false, + "external_gateway_info":{ + "network_id":"8ca37218-28ff-41cb-9b10-039601ea7e6b" + } + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "router": { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b" + }, + "name": "foo_router", + "admin_state_up": false, + "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3", + "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e" + } +} + `) + }) + + asu := false + gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"} + + options := CreateOpts{ + Name: "foo_router", + AdminStateUp: &asu, + GatewayInfo: &gwi, + } + r, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "foo_router", r.Name) + th.AssertEquals(t, false, r.AdminStateUp) + th.AssertDeepEquals(t, GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}, r.GatewayInfo) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/a07eea83-7710-4860-931b-5fe220fae533", 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, ` +{ + "router": { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "85d76829-6415-48ff-9c63-5c5ca8c61ac6" + }, + "name": "router1", + "admin_state_up": true, + "tenant_id": "d6554fe62e2f41efbb6e026fad5c1542", + "id": "a07eea83-7710-4860-931b-5fe220fae533" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "a07eea83-7710-4860-931b-5fe220fae533").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Status, "ACTIVE") + th.AssertDeepEquals(t, n.GatewayInfo, GatewayInfo{NetworkID: "85d76829-6415-48ff-9c63-5c5ca8c61ac6"}) + th.AssertEquals(t, n.Name, "router1") + th.AssertEquals(t, n.AdminStateUp, true) + th.AssertEquals(t, n.TenantID, "d6554fe62e2f41efbb6e026fad5c1542") + th.AssertEquals(t, n.ID, "a07eea83-7710-4860-931b-5fe220fae533") +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/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, ` +{ + "router": { + "name": "new_name", + "external_gateway_info": { + "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b" + } + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "router": { + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b" + }, + "name": "new_name", + "admin_state_up": true, + "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3", + "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e" + } +} + `) + }) + + gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"} + options := UpdateOpts{Name: "new_name", GatewayInfo: &gwi} + + n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Name, "new_name") + th.AssertDeepEquals(t, n.GatewayInfo, GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/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) +} + +func TestAddInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/add_router_interface", 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_id": "a2f1f29d-571b-4533-907f-5803ab96ead1" +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31", + "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770" +} +`) + }) + + opts := InterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"} + res, err := AddInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID) + th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID) + th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID) + th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID) +} + +func TestAddInterfaceRequiredOpts(t *testing.T) { + _, err := AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } + _, err = AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{SubnetID: "bar", PortID: "baz"}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestRemoveInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/remove_router_interface", 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_id": "a2f1f29d-571b-4533-907f-5803ab96ead1" +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188", + "tenant_id": "017d8de156df4177889f31a9bd6edc00", + "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31", + "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770" +} +`) + }) + + opts := InterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"} + res, err := RemoveInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID) + th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID) + th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID) + th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go new file mode 100644 index 000000000000..bdad4cb2fd6f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/results.go @@ -0,0 +1,161 @@ +package routers + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// GatewayInfo represents the information of an external gateway for any +// particular network router. +type GatewayInfo struct { + NetworkID string `json:"network_id" mapstructure:"network_id"` +} + +// Router represents a Neutron router. A router is a logical entity that +// forwards packets across internal subnets and NATs (network address +// translation) them on external networks through an appropriate gateway. +// +// A router has an interface for each subnet with which it is associated. By +// default, the IP address of such interface is the subnet's gateway IP. Also, +// whenever a router is associated with a subnet, a port for that router +// interface is added to the subnet's network. +type Router struct { + // Indicates whether or not a router is currently operational. + Status string `json:"status" mapstructure:"status"` + + // Information on external gateway for the router. + GatewayInfo GatewayInfo `json:"external_gateway_info" mapstructure:"external_gateway_info"` + + // Administrative state of the router. + AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + + // Human readable name for the router. Does not have to be unique. + Name string `json:"name" mapstructure:"name"` + + // Unique identifier for the router. + ID string `json:"id" mapstructure:"id"` + + // Owner of the router. Only admin users can specify a tenant identifier + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` +} + +// RouterPage is the page returned by a pager when traversing over a +// collection of routers. +type RouterPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p RouterPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"routers_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a RouterPage struct is empty. +func (p RouterPage) IsEmpty() (bool, error) { + is, err := ExtractRouters(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractRouters accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRouters(page pagination.Page) ([]Router, error) { + var resp struct { + Routers []Router `mapstructure:"routers" json:"routers"` + } + + err := mapstructure.Decode(page.(RouterPage).Body, &resp) + + return resp.Routers, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Router, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Router *Router `json:"router"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Router, 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 { + gophercloud.ErrResult +} + +// InterfaceInfo represents information about a particular router interface. As +// mentioned above, in order for a router to forward to a subnet, it needs an +// interface. +type InterfaceInfo struct { + // The ID of the subnet which this interface is associated with. + SubnetID string `json:"subnet_id" mapstructure:"subnet_id"` + + // The ID of the port that is a part of the subnet. + PortID string `json:"port_id" mapstructure:"port_id"` + + // The UUID of the interface. + ID string `json:"id" mapstructure:"id"` + + // Owner of the interface. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` +} + +// InterfaceResult represents the result of interface operations, such as +// AddInterface() and RemoveInterface(). +type InterfaceResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts an information struct. +func (r InterfaceResult) Extract() (*InterfaceInfo, error) { + if r.Err != nil { + return nil, r.Err + } + + var res *InterfaceInfo + err := mapstructure.Decode(r.Body, &res) + + return res, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go new file mode 100644 index 000000000000..bc22c2a8a821 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers/urls.go @@ -0,0 +1,21 @@ +package routers + +import "github.com/rackspace/gophercloud" + +const resourcePath = "routers" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func addInterfaceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "add_router_interface") +} + +func removeInterfaceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "remove_router_interface") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go new file mode 100644 index 000000000000..bc1fc282f4ee --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/doc.go @@ -0,0 +1,3 @@ +// Package lbaas provides information and interaction with the Load Balancer +// as a Service extension for the OpenStack Networking service. +package lbaas 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 new file mode 100644 index 000000000000..848938f9837e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests.go @@ -0,0 +1,123 @@ +package members + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Weight int `q:"weight"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + PoolID string `q:"pool_id"` + Address string `q:"address"` + ProtocolPort int `q:"protocol_port"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// pools. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those pools that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return MemberPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOpts contains all the values needed to create a new pool member. +type CreateOpts struct { + // Only required if the caller has an admin role and wants to create a pool + // for another tenant. + TenantID string + + // Required. The IP address of the member. + Address string + + // Required. The port on which the application is hosted. + ProtocolPort int + + // Required. The pool to which this member will belong. + PoolID string +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// load balancer pool member. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + type member struct { + TenantID string `json:"tenant_id,omitempty"` + ProtocolPort int `json:"protocol_port"` + Address string `json:"address"` + PoolID string `json:"pool_id"` + } + type request struct { + Member member `json:"member"` + } + + reqBody := request{Member: member{ + Address: opts.Address, + TenantID: opts.TenantID, + ProtocolPort: opts.ProtocolPort, + PoolID: opts.PoolID, + }} + + var res CreateResult + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular pool member based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// UpdateOpts contains the values used when updating a pool member. +type UpdateOpts struct { + // The administrative state of the member, which is up (true) or down (false). + AdminStateUp bool +} + +// Update allows members to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + type member struct { + AdminStateUp bool `json:"admin_state_up"` + } + type request struct { + Member member `json:"member"` + } + + reqBody := request{Member: member{AdminStateUp: opts.AdminStateUp}} + + // Send request to API + var res UpdateResult + _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + return res +} + +// Delete will permanently delete a particular member based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests_test.go new file mode 100644 index 000000000000..dc1ece321ff9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/requests_test.go @@ -0,0 +1,243 @@ +package members + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/lb/members", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members", 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, ` +{ + "members":[ + { + "status":"ACTIVE", + "weight":1, + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab", + "address":"10.0.0.4", + "protocol_port":80, + "id":"701b531b-111a-4f21-ad85-4795b7b12af6" + }, + { + "status":"ACTIVE", + "weight":1, + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab", + "address":"10.0.0.3", + "protocol_port":80, + "id":"beb53b4d-230b-4abd-8118-575b8fa006ef" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractMembers(page) + if err != nil { + t.Errorf("Failed to extract members: %v", err) + return false, err + } + + expected := []Member{ + Member{ + Status: "ACTIVE", + Weight: 1, + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + PoolID: "72741b06-df4d-4715-b142-276b6bce75ab", + Address: "10.0.0.4", + ProtocolPort: 80, + ID: "701b531b-111a-4f21-ad85-4795b7b12af6", + }, + Member{ + Status: "ACTIVE", + Weight: 1, + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + PoolID: "72741b06-df4d-4715-b142-276b6bce75ab", + Address: "10.0.0.3", + ProtocolPort: 80, + ID: "beb53b4d-230b-4abd-8118-575b8fa006ef", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members", 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, ` +{ + "member": { + "tenant_id": "453105b9-1754-413f-aab1-55f1af620750", + "pool_id": "foo", + "address": "192.0.2.14", + "protocol_port":8080 + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "member": { + "id": "975592ca-e308-48ad-8298-731935ee9f45", + "address": "192.0.2.14", + "protocol_port": 8080, + "tenant_id": "453105b9-1754-413f-aab1-55f1af620750", + "admin_state_up":true, + "weight": 1, + "status": "DOWN" + } +} + `) + }) + + options := CreateOpts{ + TenantID: "453105b9-1754-413f-aab1-55f1af620750", + Address: "192.0.2.14", + ProtocolPort: 8080, + PoolID: "foo", + } + _, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members/975592ca-e308-48ad-8298-731935ee9f45", 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, ` +{ + "member":{ + "id":"975592ca-e308-48ad-8298-731935ee9f45", + "address":"192.0.2.14", + "protocol_port":8080, + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "admin_state_up":true, + "weight":1, + "status":"DOWN" + } +} + `) + }) + + m, err := Get(fake.ServiceClient(), "975592ca-e308-48ad-8298-731935ee9f45").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "975592ca-e308-48ad-8298-731935ee9f45", m.ID) + th.AssertEquals(t, "192.0.2.14", m.Address) + th.AssertEquals(t, 8080, m.ProtocolPort) + th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", m.TenantID) + th.AssertEquals(t, true, m.AdminStateUp) + th.AssertEquals(t, 1, m.Weight) + th.AssertEquals(t, "DOWN", m.Status) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", 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, ` +{ + "member":{ + "admin_state_up":false + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "member":{ + "status":"PENDING_UPDATE", + "protocol_port":8080, + "weight":1, + "admin_state_up":false, + "tenant_id":"4fd44f30292945e481c7b8a0c8908869", + "pool_id":"7803631d-f181-4500-b3a2-1b68ba2a75fd", + "address":"10.0.0.5", + "status_description":null, + "id":"48a471ea-64f1-4eb6-9be7-dae6bbe40a0f" + } +} + `) + }) + + options := UpdateOpts{AdminStateUp: false} + + _, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract() + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", 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(), "332abe93-f488-41ba-870b-2ac66be7f853") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go new file mode 100644 index 000000000000..3cad339b770f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/results.go @@ -0,0 +1,122 @@ +package members + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Member represents the application running on a backend server. +type Member struct { + // The status of the member. Indicates whether the member is operational. + Status string + + // Weight of member. + Weight int + + // The administrative state of the member, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + + // Owner of the member. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` + + // The pool to which the member belongs. + PoolID string `json:"pool_id" mapstructure:"pool_id"` + + // The IP address of the member. + Address string + + // The port on which the application is hosted. + ProtocolPort int `json:"protocol_port" mapstructure:"protocol_port"` + + // The unique ID for the member. + ID string +} + +// MemberPage is the page returned by a pager when traversing over a +// collection of pool members. +type MemberPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of members has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p MemberPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"members_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a MemberPage struct is empty. +func (p MemberPage) IsEmpty() (bool, error) { + is, err := ExtractMembers(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractMembers accepts a Page struct, specifically a MemberPage struct, +// and extracts the elements into a slice of Member structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMembers(page pagination.Page) ([]Member, error) { + var resp struct { + Members []Member `mapstructure:"members" json:"members"` + } + + err := mapstructure.Decode(page.(MemberPage).Body, &resp) + if err != nil { + return nil, err + } + + return resp.Members, nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Member, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Member *Member `json:"member"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Member, 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 { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go new file mode 100644 index 000000000000..94b57e4c586e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members/urls.go @@ -0,0 +1,16 @@ +package members + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "members" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go new file mode 100644 index 000000000000..71b21ef16ea9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests.go @@ -0,0 +1,265 @@ +package monitors + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + TenantID string `q:"tenant_id"` + Type string `q:"type"` + Delay int `q:"delay"` + Timeout int `q:"timeout"` + MaxRetries int `q:"max_retries"` + HTTPMethod string `q:"http_method"` + URLPath string `q:"url_path"` + ExpectedCodes string `q:"expected_codes"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return MonitorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Constants that represent approved monitoring types. +const ( + TypePING = "PING" + TypeTCP = "TCP" + TypeHTTP = "HTTP" + TypeHTTPS = "HTTPS" +) + +var ( + errValidTypeRequired = fmt.Errorf("A valid Type is required. Supported values are PING, TCP, HTTP and HTTPS") + errDelayRequired = fmt.Errorf("Delay is required") + errTimeoutRequired = fmt.Errorf("Timeout is required") + errMaxRetriesRequired = fmt.Errorf("MaxRetries is required") + errURLPathRequired = fmt.Errorf("URL path is required") + errExpectedCodesRequired = fmt.Errorf("ExpectedCodes is required") + errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout") +) + +// CreateOpts contains all the values needed to create a new health monitor. +type CreateOpts struct { + // Required for admins. Indicates the owner of the VIP. + TenantID string + + // Required. The type of probe, which is PING, TCP, HTTP, or HTTPS, that is + // sent by the load balancer to verify the member state. + Type string + + // Required. The time, in seconds, between sending probes to members. + Delay int + + // Required. Maximum number of seconds for a monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int + + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int + + // Required for HTTP(S) types. URI path that will be accessed if monitor type + // is HTTP or HTTPS. + URLPath string + + // Required for HTTP(S) types. The HTTP method used for requests by the + // monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string + + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string + + AdminStateUp *bool +} + +// Create is an operation which provisions a new health monitor. There are +// different types of monitor you can provision: PING, TCP or HTTP(S). Below +// are examples of how to create each one. +// +// Here is an example config struct to use when creating a PING or TCP monitor: +// +// CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3} +// CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3} +// +// Here is an example config struct to use when creating a HTTP(S) monitor: +// +// CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3, +// HttpMethod: "HEAD", ExpectedCodes: "200"} +// +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate inputs + allowed := map[string]bool{TypeHTTP: true, TypeHTTPS: true, TypeTCP: true, TypePING: true} + if opts.Type == "" || allowed[opts.Type] == false { + res.Err = errValidTypeRequired + } + if opts.Delay == 0 { + res.Err = errDelayRequired + } + if opts.Timeout == 0 { + res.Err = errTimeoutRequired + } + if opts.MaxRetries == 0 { + res.Err = errMaxRetriesRequired + } + if opts.Type == TypeHTTP || opts.Type == TypeHTTPS { + if opts.URLPath == "" { + res.Err = errURLPathRequired + } + if opts.ExpectedCodes == "" { + res.Err = errExpectedCodesRequired + } + } + if opts.Delay < opts.Timeout { + res.Err = errDelayMustGETimeout + } + if res.Err != nil { + return res + } + + type monitor struct { + Type string `json:"type"` + Delay int `json:"delay"` + Timeout int `json:"timeout"` + MaxRetries int `json:"max_retries"` + TenantID *string `json:"tenant_id,omitempty"` + URLPath *string `json:"url_path,omitempty"` + ExpectedCodes *string `json:"expected_codes,omitempty"` + HTTPMethod *string `json:"http_method,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + } + + type request struct { + Monitor monitor `json:"health_monitor"` + } + + reqBody := request{Monitor: monitor{ + Type: opts.Type, + Delay: opts.Delay, + Timeout: opts.Timeout, + MaxRetries: opts.MaxRetries, + TenantID: gophercloud.MaybeString(opts.TenantID), + URLPath: gophercloud.MaybeString(opts.URLPath), + ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes), + HTTPMethod: gophercloud.MaybeString(opts.HTTPMethod), + AdminStateUp: opts.AdminStateUp, + }} + + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular health monitor based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// UpdateOpts contains all the values needed to update an existing virtual IP. +// Attributes not listed here but appear in CreateOpts are immutable and cannot +// be updated. +type UpdateOpts struct { + // Required. The time, in seconds, between sending probes to members. + Delay int + + // Required. Maximum number of seconds for a monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int + + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int + + // Required for HTTP(S) types. URI path that will be accessed if monitor type + // is HTTP or HTTPS. + URLPath string + + // Required for HTTP(S) types. The HTTP method used for requests by the + // monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string + + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string + + AdminStateUp *bool +} + +// Update is an operation which modifies the attributes of the specified monitor. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + var res UpdateResult + + if opts.Delay > 0 && opts.Timeout > 0 && opts.Delay < opts.Timeout { + res.Err = errDelayMustGETimeout + } + + type monitor struct { + Delay int `json:"delay"` + Timeout int `json:"timeout"` + MaxRetries int `json:"max_retries"` + URLPath *string `json:"url_path,omitempty"` + ExpectedCodes *string `json:"expected_codes,omitempty"` + HTTPMethod *string `json:"http_method,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + } + + type request struct { + Monitor monitor `json:"health_monitor"` + } + + reqBody := request{Monitor: monitor{ + Delay: opts.Delay, + Timeout: opts.Timeout, + MaxRetries: opts.MaxRetries, + URLPath: gophercloud.MaybeString(opts.URLPath), + ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes), + HTTPMethod: gophercloud.MaybeString(opts.HTTPMethod), + AdminStateUp: opts.AdminStateUp, + }} + + _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + + return res +} + +// Delete will permanently delete a particular monitor based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go new file mode 100644 index 000000000000..79a99bf8a25d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go @@ -0,0 +1,312 @@ +package monitors + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.AssertEquals(t, th.Endpoint()+"v2.0/lb/health_monitors", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors", 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, ` +{ + "health_monitors":[ + { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":10, + "max_retries":1, + "timeout":1, + "type":"PING", + "id":"466c8345-28d8-4f84-a246-e04380b0461d" + }, + { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":5, + "expected_codes":"200", + "max_retries":2, + "http_method":"GET", + "timeout":2, + "url_path":"/", + "type":"HTTP", + "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractMonitors(page) + if err != nil { + t.Errorf("Failed to extract monitors: %v", err) + return false, err + } + + expected := []Monitor{ + Monitor{ + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 10, + MaxRetries: 1, + Timeout: 1, + Type: "PING", + ID: "466c8345-28d8-4f84-a246-e04380b0461d", + }, + Monitor{ + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 5, + ExpectedCodes: "200", + MaxRetries: 2, + Timeout: 2, + URLPath: "/", + Type: "HTTP", + HTTPMethod: "GET", + ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestDelayMustBeGreaterOrEqualThanTimeout(t *testing.T) { + _, err := Create(fake.ServiceClient(), CreateOpts{ + Type: "HTTP", + Delay: 1, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } + + _, err = Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", UpdateOpts{ + Delay: 1, + Timeout: 10, + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors", 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, ` +{ + "health_monitor":{ + "type":"HTTP", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "delay":20, + "timeout":10, + "max_retries":5, + "url_path":"/check", + "expected_codes":"200-299" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "health_monitor":{ + "id":"f3eeab00-8367-4524-b662-55e64d4cacb5", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "type":"HTTP", + "delay":20, + "timeout":10, + "max_retries":5, + "http_method":"GET", + "url_path":"/check", + "expected_codes":"200-299", + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + _, err := Create(fake.ServiceClient(), CreateOpts{ + Type: "HTTP", + TenantID: "453105b9-1754-413f-aab1-55f1af620750", + Delay: 20, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + + th.AssertNoErr(t, err) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Type: TypeHTTP}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors/f3eeab00-8367-4524-b662-55e64d4cacb5", 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, ` +{ + "health_monitor":{ + "id":"f3eeab00-8367-4524-b662-55e64d4cacb5", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "type":"HTTP", + "delay":20, + "timeout":10, + "max_retries":5, + "http_method":"GET", + "url_path":"/check", + "expected_codes":"200-299", + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + hm, err := Get(fake.ServiceClient(), "f3eeab00-8367-4524-b662-55e64d4cacb5").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "f3eeab00-8367-4524-b662-55e64d4cacb5", hm.ID) + th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", hm.TenantID) + th.AssertEquals(t, "HTTP", hm.Type) + th.AssertEquals(t, 20, hm.Delay) + th.AssertEquals(t, 10, hm.Timeout) + th.AssertEquals(t, 5, hm.MaxRetries) + th.AssertEquals(t, "GET", hm.HTTPMethod) + th.AssertEquals(t, "/check", hm.URLPath) + th.AssertEquals(t, "200-299", hm.ExpectedCodes) + th.AssertEquals(t, true, hm.AdminStateUp) + th.AssertEquals(t, "ACTIVE", hm.Status) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", 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, ` +{ + "health_monitor":{ + "delay": 3, + "timeout": 20, + "max_retries": 10, + "url_path": "/another_check", + "expected_codes": "301" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "health_monitor": { + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "delay": 3, + "max_retries": 10, + "http_method": "GET", + "timeout": 20, + "pools": [ + { + "status": "PENDING_CREATE", + "status_description": null, + "pool_id": "6e55751f-6ad4-4e53-b8d4-02e442cd21df" + } + ], + "type": "PING", + "id": "b05e44b5-81f9-4551-b474-711a722698f7" + } +} + `) + }) + + _, err := Update(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7", UpdateOpts{ + Delay: 3, + Timeout: 20, + MaxRetries: 10, + URLPath: "/another_check", + ExpectedCodes: "301", + }).Extract() + + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", 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(), "b05e44b5-81f9-4551-b474-711a722698f7") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go new file mode 100644 index 000000000000..d595abd54075 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/results.go @@ -0,0 +1,147 @@ +package monitors + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Monitor represents a load balancer health monitor. A health monitor is used +// to determine whether or not back-end members of the VIP's pool are usable +// for processing a request. A pool can have several health monitors associated +// with it. There are different types of health monitors supported: +// +// PING: used to ping the members using ICMP. +// TCP: used to connect to the members using TCP. +// HTTP: used to send an HTTP request to the member. +// HTTPS: used to send a secure HTTP request to the member. +// +// When a pool has several monitors associated with it, each member of the pool +// is monitored by all these monitors. If any monitor declares the member as +// unhealthy, then the member status is changed to INACTIVE and the member +// won't participate in its pool's load balancing. In other words, ALL monitors +// must declare the member to be healthy for it to stay ACTIVE. +type Monitor struct { + // The unique ID for the VIP. + ID string + + // Owner of the VIP. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` + + // The type of probe sent by the load balancer to verify the member state, + // which is PING, TCP, HTTP, or HTTPS. + Type string + + // The time, in seconds, between sending probes to members. + Delay int + + // The maximum number of seconds for a monitor to wait for a connection to be + // established before it times out. This value must be less than the delay value. + Timeout int + + // Number of allowed connection failures before changing the status of the + // member to INACTIVE. A valid value is from 1 to 10. + MaxRetries int `json:"max_retries" mapstructure:"max_retries"` + + // The HTTP method that the monitor uses for requests. + HTTPMethod string `json:"http_method" mapstructure:"http_method"` + + // The HTTP path of the request sent by the monitor to test the health of a + // member. Must be a string beginning with a forward slash (/). + URLPath string `json:"url_path" mapstructure:"url_path"` + + // Expected HTTP codes for a passing HTTP(S) monitor. + ExpectedCodes string `json:"expected_codes" mapstructure:"expected_codes"` + + // The administrative state of the health monitor, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + + // The status of the health monitor. Indicates whether the health monitor is + // operational. + Status string +} + +// MonitorPage is the page returned by a pager when traversing over a +// collection of health monitors. +type MonitorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of monitors has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p MonitorPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"health_monitors_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (p MonitorPage) IsEmpty() (bool, error) { + is, err := ExtractMonitors(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct, +// and extracts the elements into a slice of Monitor structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMonitors(page pagination.Page) ([]Monitor, error) { + var resp struct { + Monitors []Monitor `mapstructure:"health_monitors" json:"health_monitors"` + } + + err := mapstructure.Decode(page.(MonitorPage).Body, &resp) + + return resp.Monitors, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a monitor. +func (r commonResult) Extract() (*Monitor, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Monitor *Monitor `json:"health_monitor" mapstructure:"health_monitor"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Monitor, 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 { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go new file mode 100644 index 000000000000..46e84bbf5219 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors/urls.go @@ -0,0 +1,16 @@ +package monitors + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "health_monitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go new file mode 100644 index 000000000000..2bb0acc447fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests.go @@ -0,0 +1,181 @@ +package pools + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + LBMethod string `q:"lb_method"` + Protocol string `q:"protocol"` + SubnetID string `q:"subnet_id"` + TenantID string `q:"tenant_id"` + AdminStateUp *bool `q:"admin_state_up"` + Name string `q:"name"` + ID string `q:"id"` + VIPID string `q:"vip_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// pools. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those pools that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return PoolPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Supported attributes for create/update operations. +const ( + LBMethodRoundRobin = "ROUND_ROBIN" + LBMethodLeastConnections = "LEAST_CONNECTIONS" + + ProtocolTCP = "TCP" + ProtocolHTTP = "HTTP" + ProtocolHTTPS = "HTTPS" +) + +// CreateOpts contains all the values needed to create a new pool. +type CreateOpts struct { + // Only required if the caller has an admin role and wants to create a pool + // for another tenant. + TenantID string + + // Required. Name of the pool. + Name string + + // Required. The protocol used by the pool members, you can use either + // ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS. + Protocol string + + // The network on which the members of the pool will be located. Only members + // that are on this network can be added to the pool. + SubnetID string + + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin and + // LBMethodLeastConnections as valid values for this attribute. + LBMethod string +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// load balancer pool. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + type pool struct { + Name string `json:"name"` + TenantID string `json:"tenant_id,omitempty"` + Protocol string `json:"protocol"` + SubnetID string `json:"subnet_id"` + LBMethod string `json:"lb_method"` + } + type request struct { + Pool pool `json:"pool"` + } + + reqBody := request{Pool: pool{ + Name: opts.Name, + TenantID: opts.TenantID, + Protocol: opts.Protocol, + SubnetID: opts.SubnetID, + LBMethod: opts.LBMethod, + }} + + var res CreateResult + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular pool based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// UpdateOpts contains the values used when updating a pool. +type UpdateOpts struct { + // Required. Name of the pool. + Name string + + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin and + // LBMethodLeastConnections as valid values for this attribute. + LBMethod string +} + +// Update allows pools to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + type pool struct { + Name string `json:"name,"` + LBMethod string `json:"lb_method"` + } + type request struct { + Pool pool `json:"pool"` + } + + reqBody := request{Pool: pool{ + Name: opts.Name, + LBMethod: opts.LBMethod, + }} + + // Send request to API + var res UpdateResult + _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Delete will permanently delete a particular pool based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} + +// AssociateMonitor will associate a health monitor with a particular pool. +// Once associated, the health monitor will start monitoring the members of the +// pool and will deactivate these members if they are deemed unhealthy. A +// member can be deactivated (status set to INACTIVE) if any of health monitors +// finds it unhealthy. +func AssociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) AssociateResult { + type hm struct { + ID string `json:"id"` + } + type request struct { + Monitor hm `json:"health_monitor"` + } + + reqBody := request{hm{ID: monitorID}} + + var res AssociateResult + _, res.Err = c.Post(associateURL(c, poolID), reqBody, &res.Body, nil) + return res +} + +// DisassociateMonitor will disassociate a health monitor with a particular +// pool. When dissociation is successful, the health monitor will no longer +// check for the health of the members of the pool. +func DisassociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) AssociateResult { + var res AssociateResult + _, res.Err = c.Delete(disassociateURL(c, poolID, monitorID), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go new file mode 100644 index 000000000000..6da29a6b8e64 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/requests_test.go @@ -0,0 +1,317 @@ +package pools + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/lb/pools", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools", 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, ` +{ + "pools":[ + { + "status":"ACTIVE", + "lb_method":"ROUND_ROBIN", + "protocol":"HTTP", + "description":"", + "health_monitors":[ + "466c8345-28d8-4f84-a246-e04380b0461d", + "5d4b5228-33b0-4e60-b225-9b727c1a20e7" + ], + "members":[ + "701b531b-111a-4f21-ad85-4795b7b12af6", + "beb53b4d-230b-4abd-8118-575b8fa006ef" + ], + "status_description": null, + "id":"72741b06-df4d-4715-b142-276b6bce75ab", + "vip_id":"4ec89087-d057-4e2c-911f-60a3b47ee304", + "name":"app_pool", + "admin_state_up":true, + "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "health_monitors_status": [], + "provider": "haproxy" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractPools(page) + if err != nil { + t.Errorf("Failed to extract pools: %v", err) + return false, err + } + + expected := []Pool{ + Pool{ + Status: "ACTIVE", + LBMethod: "ROUND_ROBIN", + Protocol: "HTTP", + Description: "", + MonitorIDs: []string{ + "466c8345-28d8-4f84-a246-e04380b0461d", + "5d4b5228-33b0-4e60-b225-9b727c1a20e7", + }, + SubnetID: "8032909d-47a1-4715-90af-5153ffe39861", + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + AdminStateUp: true, + Name: "app_pool", + MemberIDs: []string{ + "701b531b-111a-4f21-ad85-4795b7b12af6", + "beb53b4d-230b-4abd-8118-575b8fa006ef", + }, + ID: "72741b06-df4d-4715-b142-276b6bce75ab", + VIPID: "4ec89087-d057-4e2c-911f-60a3b47ee304", + Provider: "haproxy", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools", 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, ` +{ + "pool": { + "lb_method": "ROUND_ROBIN", + "protocol": "HTTP", + "name": "Example pool", + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "pool": { + "status": "PENDING_CREATE", + "lb_method": "ROUND_ROBIN", + "protocol": "HTTP", + "description": "", + "health_monitors": [], + "members": [], + "status_description": null, + "id": "69055154-f603-4a28-8951-7cc2d9e54a9a", + "vip_id": null, + "name": "Example pool", + "admin_state_up": true, + "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9", + "tenant_id": "2ffc6e22aae24e4795f87155d24c896f", + "health_monitors_status": [] + } +} + `) + }) + + options := CreateOpts{ + LBMethod: LBMethodRoundRobin, + Protocol: "HTTP", + Name: "Example pool", + SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9", + TenantID: "2ffc6e22aae24e4795f87155d24c896f", + } + p, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "PENDING_CREATE", p.Status) + th.AssertEquals(t, "ROUND_ROBIN", p.LBMethod) + th.AssertEquals(t, "HTTP", p.Protocol) + th.AssertEquals(t, "", p.Description) + th.AssertDeepEquals(t, []string{}, p.MonitorIDs) + th.AssertDeepEquals(t, []string{}, p.MemberIDs) + th.AssertEquals(t, "69055154-f603-4a28-8951-7cc2d9e54a9a", p.ID) + th.AssertEquals(t, "Example pool", p.Name) + th.AssertEquals(t, "1981f108-3c48-48d2-b908-30f7d28532c9", p.SubnetID) + th.AssertEquals(t, "2ffc6e22aae24e4795f87155d24c896f", p.TenantID) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", 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, ` +{ + "pool":{ + "id":"332abe93-f488-41ba-870b-2ac66be7f853", + "tenant_id":"19eaa775-cf5d-49bc-902e-2f85f668d995", + "name":"Example pool", + "description":"", + "protocol":"tcp", + "lb_algorithm":"ROUND_ROBIN", + "session_persistence":{ + }, + "healthmonitor_id":null, + "members":[ + ], + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + n, err := Get(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.ID, "332abe93-f488-41ba-870b-2ac66be7f853") +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", 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, ` +{ + "pool":{ + "name":"SuperPool", + "lb_method": "LEAST_CONNECTIONS" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "pool":{ + "status":"PENDING_UPDATE", + "lb_method":"LEAST_CONNECTIONS", + "protocol":"TCP", + "description":"", + "health_monitors":[ + + ], + "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "admin_state_up":true, + "name":"SuperPool", + "members":[ + + ], + "id":"61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "vip_id":null + } +} + `) + }) + + options := UpdateOpts{Name: "SuperPool", LBMethod: LBMethodLeastConnections} + + n, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "SuperPool", n.Name) + th.AssertDeepEquals(t, "LEAST_CONNECTIONS", n.LBMethod) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", 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(), "332abe93-f488-41ba-870b-2ac66be7f853") + th.AssertNoErr(t, res.Err) +} + +func TestAssociateHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors", 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, ` +{ + "health_monitor":{ + "id":"b624decf-d5d3-4c66-9a3d-f047e7786181" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + }) + + _, err := AssociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181").Extract() + th.AssertNoErr(t, err) +} + +func TestDisassociateHealthMonitor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors/b624decf-d5d3-4c66-9a3d-f047e7786181", 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 := DisassociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go new file mode 100644 index 000000000000..07ec85eda4a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/results.go @@ -0,0 +1,146 @@ +package pools + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Pool represents a logical set of devices, such as web servers, that you +// group together to receive and process traffic. The load balancing function +// chooses a member of the pool according to the configured load balancing +// method to handle the new requests or connections received on the VIP address. +// There is only one pool per virtual IP. +type Pool struct { + // The status of the pool. Indicates whether the pool is operational. + Status string + + // The load-balancer algorithm, which is round-robin, least-connections, and + // so on. This value, which must be supported, is dependent on the provider. + // Round-robin must be supported. + LBMethod string `json:"lb_method" mapstructure:"lb_method"` + + // The protocol of the pool, which is TCP, HTTP, or HTTPS. + Protocol string + + // Description for the pool. + Description string + + // The IDs of associated monitors which check the health of the pool members. + MonitorIDs []string `json:"health_monitors" mapstructure:"health_monitors"` + + // The network on which the members of the pool will be located. Only members + // that are on this network can be added to the pool. + SubnetID string `json:"subnet_id" mapstructure:"subnet_id"` + + // Owner of the pool. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` + + // The administrative state of the pool, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + + // Pool name. Does not have to be unique. + Name string + + // List of member IDs that belong to the pool. + MemberIDs []string `json:"members" mapstructure:"members"` + + // The unique ID for the pool. + ID string + + // The ID of the virtual IP associated with this pool + VIPID string `json:"vip_id" mapstructure:"vip_id"` + + // The provider + Provider string +} + +// PoolPage is the page returned by a pager when traversing over a +// collection of pools. +type PoolPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of pools has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p PoolPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"pools_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (p PoolPage) IsEmpty() (bool, error) { + is, err := ExtractPools(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractPools accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPools(page pagination.Page) ([]Pool, error) { + var resp struct { + Pools []Pool `mapstructure:"pools" json:"pools"` + } + + err := mapstructure.Decode(page.(PoolPage).Body, &resp) + + return resp.Pools, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Pool, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Pool *Pool `json:"pool"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Pool, 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 { + gophercloud.ErrResult +} + +// AssociateResult represents the result of an association operation. +type AssociateResult struct { + commonResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go new file mode 100644 index 000000000000..6cd15b002612 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools/urls.go @@ -0,0 +1,25 @@ +package pools + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "pools" + monitorPath = "health_monitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func associateURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, monitorPath) +} + +func disassociateURL(c *gophercloud.ServiceClient, poolID, monitorID string) string { + return c.ServiceURL(rootPath, resourcePath, poolID, monitorPath, monitorID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go new file mode 100644 index 000000000000..6216f873e310 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests.go @@ -0,0 +1,256 @@ +package vips + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + SubnetID string `q:"subnet_id"` + Address string `q:"address"` + PortID string `q:"port_id"` + Protocol string `q:"protocol"` + ProtocolPort int `q:"protocol_port"` + ConnectionLimit int `q:"connection_limit"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return VIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +var ( + errNameRequired = fmt.Errorf("Name is required") + errSubnetIDRequried = fmt.Errorf("SubnetID is required") + errProtocolRequired = fmt.Errorf("Protocol is required") + errProtocolPortRequired = fmt.Errorf("Protocol port is required") + errPoolIDRequired = fmt.Errorf("PoolID is required") +) + +// CreateOpts contains all the values needed to create a new virtual IP. +type CreateOpts struct { + // Required. Human-readable name for the VIP. Does not have to be unique. + Name string + + // Required. The network on which to allocate the VIP's address. A tenant can + // only create VIPs on networks authorized by policy (e.g. networks that + // belong to them or networks that are shared). + SubnetID string + + // Required. The protocol - can either be TCP, HTTP or HTTPS. + Protocol string + + // Required. The port on which to listen for client traffic. + ProtocolPort int + + // Required. The ID of the pool with which the VIP is associated. + PoolID string + + // Required for admins. Indicates the owner of the VIP. + TenantID string + + // Optional. The IP address of the VIP. + Address string + + // Optional. Human-readable description for the VIP. + Description string + + // Optional. Omit this field to prevent session persistence. + Persistence *SessionPersistence + + // Optional. The maximum number of connections allowed for the VIP. + ConnLimit *int + + // Optional. The administrative state of the VIP. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool +} + +// Create is an operation which provisions a new virtual IP based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +// +// Please note that the PoolID should refer to a pool that is not already +// associated with another vip. If the pool is already used by another vip, +// then the operation will fail with a 409 Conflict error will be returned. +// +// Users with an admin role can create VIPs on behalf of other tenants by +// specifying a TenantID attribute different than their own. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate required opts + if opts.Name == "" { + res.Err = errNameRequired + return res + } + if opts.SubnetID == "" { + res.Err = errSubnetIDRequried + return res + } + if opts.Protocol == "" { + res.Err = errProtocolRequired + return res + } + if opts.ProtocolPort == 0 { + res.Err = errProtocolPortRequired + return res + } + if opts.PoolID == "" { + res.Err = errPoolIDRequired + return res + } + + type vip struct { + Name string `json:"name"` + SubnetID string `json:"subnet_id"` + Protocol string `json:"protocol"` + ProtocolPort int `json:"protocol_port"` + PoolID string `json:"pool_id"` + Description *string `json:"description,omitempty"` + TenantID *string `json:"tenant_id,omitempty"` + Address *string `json:"address,omitempty"` + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + ConnLimit *int `json:"connection_limit,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + } + + type request struct { + VirtualIP vip `json:"vip"` + } + + reqBody := request{VirtualIP: vip{ + Name: opts.Name, + SubnetID: opts.SubnetID, + Protocol: opts.Protocol, + ProtocolPort: opts.ProtocolPort, + PoolID: opts.PoolID, + Description: gophercloud.MaybeString(opts.Description), + TenantID: gophercloud.MaybeString(opts.TenantID), + Address: gophercloud.MaybeString(opts.Address), + ConnLimit: opts.ConnLimit, + AdminStateUp: opts.AdminStateUp, + }} + + if opts.Persistence != nil { + reqBody.VirtualIP.Persistence = opts.Persistence + } + + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular virtual IP based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// UpdateOpts contains all the values needed to update an existing virtual IP. +// Attributes not listed here but appear in CreateOpts are immutable and cannot +// be updated. +type UpdateOpts struct { + // Human-readable name for the VIP. Does not have to be unique. + Name string + + // Required. The ID of the pool with which the VIP is associated. + PoolID string + + // Optional. Human-readable description for the VIP. + Description string + + // Optional. Omit this field to prevent session persistence. + Persistence *SessionPersistence + + // Optional. The maximum number of connections allowed for the VIP. + ConnLimit *int + + // Optional. The administrative state of the VIP. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool +} + +// Update is an operation which modifies the attributes of the specified VIP. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + type vip struct { + Name string `json:"name,omitempty"` + PoolID string `json:"pool_id,omitempty"` + Description *string `json:"description,omitempty"` + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + ConnLimit *int `json:"connection_limit,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + } + + type request struct { + VirtualIP vip `json:"vip"` + } + + reqBody := request{VirtualIP: vip{ + Name: opts.Name, + PoolID: opts.PoolID, + Description: gophercloud.MaybeString(opts.Description), + ConnLimit: opts.ConnLimit, + AdminStateUp: opts.AdminStateUp, + }} + + if opts.Persistence != nil { + reqBody.VirtualIP.Persistence = opts.Persistence + } + + var res UpdateResult + _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + + return res +} + +// Delete will permanently delete a particular virtual IP based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests_test.go new file mode 100644 index 000000000000..430f1a1eebfe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/requests_test.go @@ -0,0 +1,336 @@ +package vips + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/lb/vips", rootURL(fake.ServiceClient())) + th.AssertEquals(t, th.Endpoint()+"v2.0/lb/vips/foo", resourceURL(fake.ServiceClient(), "foo")) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips", 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, ` +{ + "vips":[ + { + "id": "db902c0c-d5ff-4753-b465-668ad9656918", + "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c", + "name": "web_vip", + "description": "lb config for the web tier", + "subnet_id": "96a4386a-f8c3-42ed-afce-d7954eee77b3", + "address" : "10.30.176.47", + "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + "protocol": "HTTP", + "protocol_port": 80, + "pool_id" : "cfc6589d-f949-4c66-99d2-c2da56ef3764", + "admin_state_up": true, + "status": "ACTIVE" + }, + { + "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c", + "name": "db_vip", + "description": "lb config for the db tier", + "subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + "address" : "10.30.176.48", + "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + "protocol": "TCP", + "protocol_port": 3306, + "pool_id" : "41efe233-7591-43c5-9cf7-923964759f9e", + "session_persistence" : {"type" : "SOURCE_IP"}, + "connection_limit" : 2000, + "admin_state_up": true, + "status": "INACTIVE" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVIPs(page) + if err != nil { + t.Errorf("Failed to extract LBs: %v", err) + return false, err + } + + expected := []VirtualIP{ + VirtualIP{ + ID: "db902c0c-d5ff-4753-b465-668ad9656918", + TenantID: "310df60f-2a10-4ee5-9554-98393092194c", + Name: "web_vip", + Description: "lb config for the web tier", + SubnetID: "96a4386a-f8c3-42ed-afce-d7954eee77b3", + Address: "10.30.176.47", + PortID: "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + Protocol: "HTTP", + ProtocolPort: 80, + PoolID: "cfc6589d-f949-4c66-99d2-c2da56ef3764", + Persistence: SessionPersistence{}, + ConnLimit: 0, + AdminStateUp: true, + Status: "ACTIVE", + }, + VirtualIP{ + ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", + TenantID: "310df60f-2a10-4ee5-9554-98393092194c", + Name: "db_vip", + Description: "lb config for the db tier", + SubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + Address: "10.30.176.48", + PortID: "cd1f7a47-4fa6-449c-9ee7-632838aedfea", + Protocol: "TCP", + ProtocolPort: 3306, + PoolID: "41efe233-7591-43c5-9cf7-923964759f9e", + Persistence: SessionPersistence{Type: "SOURCE_IP"}, + ConnLimit: 2000, + AdminStateUp: true, + Status: "INACTIVE", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips", 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, ` +{ + "vip": { + "protocol": "HTTP", + "name": "NewVip", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "protocol_port": 80, + "session_persistence": {"type": "SOURCE_IP"} + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "vip": { + "status": "PENDING_CREATE", + "protocol": "HTTP", + "description": "", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea", + "connection_limit": -1, + "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "address": "10.0.0.11", + "protocol_port": 80, + "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", + "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2", + "name": "NewVip" + } +} + `) + }) + + opts := CreateOpts{ + Protocol: "HTTP", + Name: "NewVip", + AdminStateUp: Up, + SubnetID: "8032909d-47a1-4715-90af-5153ffe39861", + PoolID: "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + ProtocolPort: 80, + Persistence: &SessionPersistence{Type: "SOURCE_IP"}, + } + + r, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "PENDING_CREATE", r.Status) + th.AssertEquals(t, "HTTP", r.Protocol) + th.AssertEquals(t, "", r.Description) + th.AssertEquals(t, true, r.AdminStateUp) + th.AssertEquals(t, "8032909d-47a1-4715-90af-5153ffe39861", r.SubnetID) + th.AssertEquals(t, "83657cfcdfe44cd5920adaf26c48ceea", r.TenantID) + th.AssertEquals(t, -1, r.ConnLimit) + th.AssertEquals(t, "61b1f87a-7a21-4ad3-9dda-7f81d249944f", r.PoolID) + th.AssertEquals(t, "10.0.0.11", r.Address) + th.AssertEquals(t, 80, r.ProtocolPort) + th.AssertEquals(t, "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", r.PortID) + th.AssertEquals(t, "c987d2be-9a3c-4ac9-a046-e8716b1350e2", r.ID) + th.AssertEquals(t, "NewVip", r.Name) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Name: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar", ProtocolPort: 80}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", 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, ` +{ + "vip": { + "status": "ACTIVE", + "protocol": "HTTP", + "description": "", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea", + "connection_limit": 1000, + "pool_id": "72741b06-df4d-4715-b142-276b6bce75ab", + "session_persistence": { + "cookie_name": "MyAppCookie", + "type": "APP_COOKIE" + }, + "address": "10.0.0.10", + "protocol_port": 80, + "port_id": "b5a743d6-056b-468b-862d-fb13a9aa694e", + "id": "4ec89087-d057-4e2c-911f-60a3b47ee304", + "name": "my-vip" + } +} + `) + }) + + vip, err := Get(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "ACTIVE", vip.Status) + th.AssertEquals(t, "HTTP", vip.Protocol) + th.AssertEquals(t, "", vip.Description) + th.AssertEquals(t, true, vip.AdminStateUp) + th.AssertEquals(t, 1000, vip.ConnLimit) + th.AssertEquals(t, SessionPersistence{Type: "APP_COOKIE", CookieName: "MyAppCookie"}, vip.Persistence) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", 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, ` +{ + "vip": { + "connection_limit": 1000, + "session_persistence": {"type": "SOURCE_IP"} + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "vip": { + "status": "PENDING_UPDATE", + "protocol": "HTTP", + "description": "", + "admin_state_up": true, + "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861", + "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea", + "connection_limit": 1000, + "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f", + "address": "10.0.0.11", + "protocol_port": 80, + "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", + "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2", + "name": "NewVip" + } +} + `) + }) + + i1000 := 1000 + options := UpdateOpts{ + ConnLimit: &i1000, + Persistence: &SessionPersistence{Type: "SOURCE_IP"}, + } + vip, err := Update(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304", options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "PENDING_UPDATE", vip.Status) + th.AssertEquals(t, 1000, vip.ConnLimit) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", 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(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go new file mode 100644 index 000000000000..e1092e780ece --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/results.go @@ -0,0 +1,166 @@ +package vips + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// SessionPersistence represents the session persistence feature of the load +// balancing service. It attempts to force connections or requests in the same +// session to be processed by the same member as long as it is ative. Three +// types of persistence are supported: +// +// SOURCE_IP: With this mode, all connections originating from the same source +// IP address, will be handled by the same member of the pool. +// HTTP_COOKIE: With this persistence mode, the load balancing function will +// create a cookie on the first request from a client. Subsequent +// requests containing the same cookie value will be handled by +// the same member of the pool. +// APP_COOKIE: With this persistence mode, the load balancing function will +// rely on a cookie established by the backend application. All +// requests carrying the same cookie value will be handled by the +// same member of the pool. +type SessionPersistence struct { + // The type of persistence mode + Type string `mapstructure:"type" json:"type"` + + // Name of cookie if persistence mode is set appropriately + CookieName string `mapstructure:"cookie_name" json:"cookie_name,omitempty"` +} + +// VirtualIP is the primary load balancing configuration object that specifies +// the virtual IP address and port on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +// This entity is sometimes known in LB products under the name of a "virtual +// server", a "vserver" or a "listener". +type VirtualIP struct { + // The unique ID for the VIP. + ID string `mapstructure:"id" json:"id"` + + // Owner of the VIP. Only an admin user can specify a tenant ID other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` + + // Human-readable name for the VIP. Does not have to be unique. + Name string `mapstructure:"name" json:"name"` + + // Human-readable description for the VIP. + Description string `mapstructure:"description" json:"description"` + + // The ID of the subnet on which to allocate the VIP address. + SubnetID string `mapstructure:"subnet_id" json:"subnet_id"` + + // The IP address of the VIP. + Address string `mapstructure:"address" json:"address"` + + // The protocol of the VIP address. A valid value is TCP, HTTP, or HTTPS. + Protocol string `mapstructure:"protocol" json:"protocol"` + + // The port on which to listen to client traffic that is associated with the + // VIP address. A valid value is from 0 to 65535. + ProtocolPort int `mapstructure:"protocol_port" json:"protocol_port"` + + // The ID of the pool with which the VIP is associated. + PoolID string `mapstructure:"pool_id" json:"pool_id"` + + // The ID of the port which belongs to the load balancer + PortID string `mapstructure:"port_id" json:"port_id"` + + // Indicates whether connections in the same session will be processed by the + // same pool member or not. + Persistence SessionPersistence `mapstructure:"session_persistence" json:"session_persistence"` + + // The maximum number of connections allowed for the VIP. Default is -1, + // meaning no limit. + ConnLimit int `mapstructure:"connection_limit" json:"connection_limit"` + + // The administrative state of the VIP. A valid value is true (UP) or false (DOWN). + AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"` + + // The status of the VIP. Indicates whether the VIP is operational. + Status string `mapstructure:"status" json:"status"` +} + +// VIPPage is the page returned by a pager when traversing over a +// collection of routers. +type VIPPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p VIPPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"vips_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a RouterPage 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 VirtualIP structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractVIPs(page pagination.Page) ([]VirtualIP, error) { + var resp struct { + VIPs []VirtualIP `mapstructure:"vips" json:"vips"` + } + + err := mapstructure.Decode(page.(VIPPage).Body, &resp) + + return resp.VIPs, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*VirtualIP, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VirtualIP *VirtualIP `mapstructure:"vip" json:"vip"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.VirtualIP, 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 { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go new file mode 100644 index 000000000000..2b6f67e71d66 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips/urls.go @@ -0,0 +1,16 @@ +package vips + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "lb" + resourcePath = "vips" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/doc.go new file mode 100644 index 000000000000..373da44f84de --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/doc.go @@ -0,0 +1,21 @@ +// Package provider gives access to the provider Neutron plugin, allowing +// network extended attributes. The provider extended attributes for networks +// enable administrative users to specify how network objects map to the +// underlying networking infrastructure. These extended attributes also appear +// when administrative users query networks. +// +// For more information about extended attributes, see the NetworkExtAttrs +// struct. The actual semantics of these attributes depend on the technology +// back end of the particular plug-in. See the plug-in documentation and the +// OpenStack Cloud Administrator Guide to understand which values should be +// specific for each of these attributes when OpenStack Networking is deployed +// with a particular plug-in. The examples shown in this chapter refer to the +// Open vSwitch plug-in. +// +// The default policy settings enable only users with administrative rights to +// specify these parameters in requests and to see their values in responses. By +// default, the provider network extension attributes are completely hidden from +// regular tenants. As a rule of thumb, if these attributes are not visible in a +// GET /networks/ operation, this implies the user submitting the +// request is not authorized to view or manipulate provider network attributes. +package provider diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results.go new file mode 100644 index 000000000000..f07d6285dba2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results.go @@ -0,0 +1,124 @@ +package provider + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +// NetworkExtAttrs represents an extended form of a Network with additional fields. +type NetworkExtAttrs struct { + // UUID for the network + ID string `mapstructure:"id" json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `mapstructure:"name" json:"name"` + + // The administrative state of network. If false (down), the network does not forward packets. + AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `mapstructure:"status" json:"status"` + + // Subnets associated with this network. + Subnets []string `mapstructure:"subnets" json:"subnets"` + + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` + + // Specifies whether the network resource can be accessed by any tenant or not. + Shared bool `mapstructure:"shared" json:"shared"` + + // Specifies the nature of the physical network mapped to this network + // resource. Examples are flat, vlan, or gre. + NetworkType string `json:"provider:network_type" mapstructure:"provider:network_type"` + + // Identifies the physical network on top of which this network object is + // being implemented. The OpenStack Networking API does not expose any facility + // for retrieving the list of available physical networks. As an example, in + // the Open vSwitch plug-in this is a symbolic name which is then mapped to + // specific bridges on each compute host through the Open vSwitch plug-in + // configuration file. + PhysicalNetwork string `json:"provider:physical_network" mapstructure:"provider:physical_network"` + + // Identifies an isolated segment on the physical network; the nature of the + // segment depends on the segmentation model defined by network_type. For + // instance, if network_type is vlan, then this is a vlan identifier; + // otherwise, if network_type is gre, then this will be a gre key. + SegmentationID string `json:"provider:segmentation_id" mapstructure:"provider:segmentation_id"` +} + +// ExtractGet decorates a GetResult struct returned from a networks.Get() +// function with extended attributes. +func ExtractGet(r networks.GetResult) (*NetworkExtAttrs, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *NetworkExtAttrs `json:"network"` + } + + err := mapstructure.WeakDecode(r.Body, &res) + + return res.Network, err +} + +// ExtractCreate decorates a CreateResult struct returned from a networks.Create() +// function with extended attributes. +func ExtractCreate(r networks.CreateResult) (*NetworkExtAttrs, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *NetworkExtAttrs `json:"network"` + } + + err := mapstructure.WeakDecode(r.Body, &res) + + return res.Network, err +} + +// ExtractUpdate decorates a UpdateResult struct returned from a +// networks.Update() function with extended attributes. +func ExtractUpdate(r networks.UpdateResult) (*NetworkExtAttrs, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *NetworkExtAttrs `json:"network"` + } + + err := mapstructure.WeakDecode(r.Body, &res) + + return res.Network, err +} + +// ExtractList accepts a Page struct, specifically a NetworkPage struct, and +// extracts the elements into a slice of NetworkExtAttrs structs. In other +// words, a generic collection is mapped into a relevant slice. +func ExtractList(page pagination.Page) ([]NetworkExtAttrs, error) { + var resp struct { + Networks []NetworkExtAttrs `mapstructure:"networks" json:"networks"` + } + + err := mapstructure.WeakDecode(page.(networks.NetworkPage).Body, &resp) + + return resp.Networks, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results_test.go new file mode 100644 index 000000000000..80816926da63 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/provider/results_test.go @@ -0,0 +1,253 @@ +package provider + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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", + "provider:segmentation_id": null, + "provider:physical_network": null, + "provider:network_type": "local" + }, + { + "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", + "provider:segmentation_id": 1234567890, + "provider:physical_network": null, + "provider:network_type": "local" + } + ] +} + `) + }) + + count := 0 + + networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractList(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []NetworkExtAttrs{ + NetworkExtAttrs{ + Status: "ACTIVE", + Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"}, + Name: "private-network", + AdminStateUp: true, + TenantID: "4fd44f30292945e481c7b8a0c8908869", + Shared: true, + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + NetworkType: "local", + PhysicalNetwork: "", + SegmentationID: "", + }, + NetworkExtAttrs{ + Status: "ACTIVE", + Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"}, + Name: "private", + AdminStateUp: true, + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + Shared: true, + ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + NetworkType: "local", + PhysicalNetwork: "", + SegmentationID: "1234567890", + }, + } + + 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("/v2.0/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", + "provider:physical_network": null, + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "provider:network_type": "local", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null + } +} + `) + }) + + res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + n, err := ExtractGet(res) + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", n.PhysicalNetwork) + th.AssertEquals(t, "local", n.NetworkType) + th.AssertEquals(t, "", n.SegmentationID) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "provider:physical_network": null, + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "provider:network_type": "local", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null + } +} + `) + }) + + options := networks.CreateOpts{Name: "sample_network", AdminStateUp: Up} + res := networks.Create(fake.ServiceClient(), options) + n, err := ExtractCreate(res) + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", n.PhysicalNetwork) + th.AssertEquals(t, "local", n.NetworkType) + th.AssertEquals(t, "", n.SegmentationID) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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": [ + "54d6f61d-db07-451c-9ab3-b9609b6b6f0b" + ], + "name": "private-network", + "provider:physical_network": null, + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "provider:network_type": "local", + "shared": true, + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "provider:segmentation_id": null + } +} + `) + }) + + iTrue := true + options := networks.UpdateOpts{Name: "new_network_name", AdminStateUp: Down, Shared: &iTrue} + res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options) + n, err := ExtractUpdate(res) + + th.AssertNoErr(t, err) + + th.AssertEquals(t, "", n.PhysicalNetwork) + th.AssertEquals(t, "local", n.NetworkType) + th.AssertEquals(t, "", n.SegmentationID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/doc.go new file mode 100644 index 000000000000..31f744ccd7a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/doc.go @@ -0,0 +1,32 @@ +// Package security contains functionality to work with security group and +// security group rules Neutron resources. +// +// Security groups and security group rules allows administrators and tenants +// the ability to specify the type of traffic and direction (ingress/egress) +// that is allowed to pass through a port. A security group is a container for +// security group rules. +// +// When a port is created in Networking it is associated with a security group. +// If a security group is not specified the port is associated with a 'default' +// security group. By default, this group drops all ingress traffic and allows +// all egress. Rules can be added to this group in order to change the behaviour. +// +// The basic characteristics of Neutron Security Groups are: +// +// For ingress traffic (to an instance) +// - Only traffic matched with security group rules are allowed. +// - When there is no rule defined, all traffic is dropped. +// +// For egress traffic (from an instance) +// - Only traffic matched with security group rules are allowed. +// - When there is no rule defined, all egress traffic are dropped. +// - When a new security group is created, rules to allow all egress traffic +// is automatically added. +// +// "default security group" is defined for each tenant. +// - For the default security group a rule which allows intercommunication +// among hosts associated with the default security group is defined by default. +// - As a result, all egress traffic and intercommunication in the default +// group are allowed and all ingress from outside of the default group is +// dropped by default (in the default security group). +package security diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go new file mode 100644 index 000000000000..55e4b3b8043c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go @@ -0,0 +1,93 @@ +package groups + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// security groups. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return SecGroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +var ( + errNameRequired = fmt.Errorf("Name is required") +) + +// CreateOpts contains all the values needed to create a new security group. +type CreateOpts struct { + // Required. Human-readable name for the VIP. Does not have to be unique. + Name string + + // Optional. Describes the security group. + Description string +} + +// Create is an operation which provisions a new security group with default +// security group rules for the IPv4 and IPv6 ether types. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate required opts + if opts.Name == "" { + res.Err = errNameRequired + return res + } + + type secgroup struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + } + + type request struct { + SecGroup secgroup `json:"security_group"` + } + + reqBody := request{SecGroup: secgroup{ + Name: opts.Name, + Description: opts.Description, + }} + + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular security group based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// Delete will permanently delete a particular security group based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests_test.go new file mode 100644 index 000000000000..5f074c72f39d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/requests_test.go @@ -0,0 +1,213 @@ +package groups + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups", rootURL(fake.ServiceClient())) + th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups/foo", resourceURL(fake.ServiceClient(), "foo")) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", 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_groups": [ + { + "description": "default", + "id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "name": "default", + "security_group_rules": [], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractGroups(page) + if err != nil { + t.Errorf("Failed to extract secgroups: %v", err) + return false, err + } + + expected := []SecGroup{ + SecGroup{ + Description: "default", + ID: "85cc3048-abc3-43cc-89b3-377341426ac5", + Name: "default", + Rules: []rules.SecGroupRule{}, + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", 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, ` +{ + "security_group": { + "name": "new-webservers", + "description": "security group for webservers" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "security_group": { + "description": "security group for webservers", + "id": "2076db17-a522-4506-91de-c6dd8e837028", + "name": "new-webservers", + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv4", + "id": "38ce2d8e-e8f1-48bd-83c2-d33cb9f50c3d", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv6", + "id": "565b9502-12de-4ffd-91e9-68885cff6ae1", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} + `) + }) + + opts := CreateOpts{Name: "new-webservers", Description: "security group for webservers"} + _, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/85cc3048-abc3-43cc-89b3-377341426ac5", 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": "85cc3048-abc3-43cc-89b3-377341426ac5", + "name": "default", + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv4", + "id": "93aa42e5-80db-4581-9391-3a608bd0e448", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} + `) + }) + + sg, err := Get(fake.ServiceClient(), "85cc3048-abc3-43cc-89b3-377341426ac5").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "default", sg.Description) + th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sg.ID) + th.AssertEquals(t, "default", sg.Name) + th.AssertEquals(t, 2, len(sg.Rules)) + th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sg.TenantID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/4ec89087-d057-4e2c-911f-60a3b47ee304", 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(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/results.go new file mode 100644 index 000000000000..49db261c22ef --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/results.go @@ -0,0 +1,108 @@ +package groups + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/rackspace/gophercloud/pagination" +) + +// SecGroup represents a container for security group rules. +type SecGroup struct { + // The UUID for the security group. + ID string + + // Human-readable name for the security group. Might not be unique. Cannot be + // named "default" as that is automatically created for a tenant. + Name string + + // The security group description. + Description string + + // A slice of security group rules that dictate the permitted behaviour for + // traffic entering and leaving the group. + Rules []rules.SecGroupRule `json:"security_group_rules" mapstructure:"security_group_rules"` + + // Owner of the security group. Only admin users can specify a TenantID + // other than their own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` +} + +// SecGroupPage is the page returned by a pager when traversing over a +// collection of security groups. +type SecGroupPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of security groups has +// reached the end of a page and the pager seeks to traverse over a new one. In +// order to do this, it needs to construct the next page's URL. +func (p SecGroupPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"security_groups_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a SecGroupPage struct is empty. +func (p SecGroupPage) IsEmpty() (bool, error) { + is, err := ExtractGroups(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractGroups accepts a Page struct, specifically a SecGroupPage struct, +// and extracts the elements into a slice of SecGroup structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractGroups(page pagination.Page) ([]SecGroup, error) { + var resp struct { + SecGroups []SecGroup `mapstructure:"security_groups" json:"security_groups"` + } + + err := mapstructure.Decode(page.(SecGroupPage).Body, &resp) + + return resp.SecGroups, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a security group. +func (r commonResult) Extract() (*SecGroup, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + SecGroup *SecGroup `mapstructure:"security_group" json:"security_group"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.SecGroup, 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 +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go new file mode 100644 index 000000000000..84f7324f0901 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go @@ -0,0 +1,13 @@ +package groups + +import "github.com/rackspace/gophercloud" + +const rootPath = "security-groups" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go new file mode 100644 index 000000000000..0b2d10b0efeb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go @@ -0,0 +1,169 @@ +package rules + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the security group attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Direction string `q:"direction"` + EtherType string `q:"ethertype"` + ID string `q:"id"` + PortRangeMax int `q:"port_range_max"` + PortRangeMin int `q:"port_range_min"` + Protocol string `q:"protocol"` + RemoteGroupID string `q:"remote_group_id"` + RemoteIPPrefix string `q:"remote_ip_prefix"` + SecGroupID string `q:"security_group_id"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// security group rules. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return SecGroupRulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Errors +var ( + errValidDirectionRequired = fmt.Errorf("A valid Direction is required") + errValidEtherTypeRequired = fmt.Errorf("A valid EtherType is required") + errSecGroupIDRequired = fmt.Errorf("A valid SecGroupID is required") + errValidProtocolRequired = fmt.Errorf("A valid Protocol is required") +) + +// Constants useful for CreateOpts +const ( + DirIngress = "ingress" + DirEgress = "egress" + Ether4 = "IPv4" + Ether6 = "IPv6" + ProtocolTCP = "tcp" + ProtocolUDP = "udp" + ProtocolICMP = "icmp" +) + +// CreateOpts contains all the values needed to create a new security group rule. +type CreateOpts struct { + // Required. Must be either "ingress" or "egress": the direction in which the + // security group rule is applied. + Direction string + + // Required. Must be "IPv4" or "IPv6", and addresses represented in CIDR must + // match the ingress or egress rules. + EtherType string + + // Required. The security group ID to associate with this security group rule. + SecGroupID string + + // Optional. The maximum port number in the range that is matched by the + // security group rule. The PortRangeMin attribute constrains the PortRangeMax + // attribute. If the protocol is ICMP, this value must be an ICMP type. + PortRangeMax int + + // Optional. The minimum port number in the range that is matched by the + // security group rule. If the protocol is TCP or UDP, this value must be + // less than or equal to the value of the PortRangeMax attribute. If the + // protocol is ICMP, this value must be an ICMP type. + PortRangeMin int + + // Optional. The protocol that is matched by the security group rule. Valid + // values are "tcp", "udp", "icmp" or an empty string. + Protocol string + + // Optional. The remote group ID to be associated with this security group + // rule. You can specify either RemoteGroupID or RemoteIPPrefix. + RemoteGroupID string + + // Optional. The remote IP prefix to be associated with this security group + // rule. You can specify either RemoteGroupID or RemoteIPPrefix. This + // attribute matches the specified IP prefix as the source IP address of the + // IP packet. + RemoteIPPrefix string +} + +// Create is an operation which provisions a new security group with default +// security group rules for the IPv4 and IPv6 ether types. +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate required opts + if opts.Direction != DirIngress && opts.Direction != DirEgress { + res.Err = errValidDirectionRequired + return res + } + if opts.EtherType != Ether4 && opts.EtherType != Ether6 { + res.Err = errValidEtherTypeRequired + return res + } + if opts.SecGroupID == "" { + res.Err = errSecGroupIDRequired + return res + } + if opts.Protocol != "" && opts.Protocol != ProtocolTCP && opts.Protocol != ProtocolUDP && opts.Protocol != ProtocolICMP { + res.Err = errValidProtocolRequired + return res + } + + type secrule struct { + Direction string `json:"direction"` + EtherType string `json:"ethertype"` + SecGroupID string `json:"security_group_id"` + PortRangeMax int `json:"port_range_max,omitempty"` + PortRangeMin int `json:"port_range_min,omitempty"` + Protocol string `json:"protocol,omitempty"` + RemoteGroupID string `json:"remote_group_id,omitempty"` + RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"` + } + + type request struct { + SecRule secrule `json:"security_group_rule"` + } + + reqBody := request{SecRule: secrule{ + Direction: opts.Direction, + EtherType: opts.EtherType, + SecGroupID: opts.SecGroupID, + PortRangeMax: opts.PortRangeMax, + PortRangeMin: opts.PortRangeMin, + Protocol: opts.Protocol, + RemoteGroupID: opts.RemoteGroupID, + RemoteIPPrefix: opts.RemoteIPPrefix, + }} + + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular security group based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// Delete will permanently delete a particular security group based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests_test.go new file mode 100644 index 000000000000..b5afef31ed8a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/requests_test.go @@ -0,0 +1,243 @@ +package rules + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules", rootURL(fake.ServiceClient())) + th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules/foo", resourceURL(fake.ServiceClient(), "foo")) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", 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_rules": [ + { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv4", + "id": "93aa42e5-80db-4581-9391-3a608bd0e448", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract secrules: %v", err) + return false, err + } + + expected := []SecGroupRule{ + SecGroupRule{ + Direction: "egress", + EtherType: "IPv6", + ID: "3c0e45ff-adaf-4124-b083-bf390e5482ff", + PortRangeMax: 0, + PortRangeMin: 0, + Protocol: "", + RemoteGroupID: "", + RemoteIPPrefix: "", + SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + SecGroupRule{ + Direction: "egress", + EtherType: "IPv4", + ID: "93aa42e5-80db-4581-9391-3a608bd0e448", + PortRangeMax: 0, + PortRangeMin: 0, + Protocol: "", + RemoteGroupID: "", + RemoteIPPrefix: "", + SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "security_group_rule": { + "direction": "ingress", + "port_range_min": 80, + "ethertype": "IPv4", + "port_range_max": 80, + "protocol": "tcp", + "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "security_group_rule": { + "direction": "ingress", + "ethertype": "IPv4", + "id": "2bc0accf-312e-429a-956e-e4407625eb62", + "port_range_max": 80, + "port_range_min": 80, + "protocol": "tcp", + "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "remote_ip_prefix": null, + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} + `) + }) + + opts := CreateOpts{ + Direction: "ingress", + PortRangeMin: 80, + EtherType: "IPv4", + PortRangeMax: 80, + Protocol: "tcp", + RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + } + _, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), CreateOpts{Direction: "something"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: "something"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4, SecGroupID: "something", Protocol: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/3c0e45ff-adaf-4124-b083-bf390e5482ff", 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_rule": { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +} + `) + }) + + sr, err := Get(fake.ServiceClient(), "3c0e45ff-adaf-4124-b083-bf390e5482ff").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "egress", sr.Direction) + th.AssertEquals(t, "IPv6", sr.EtherType) + th.AssertEquals(t, "3c0e45ff-adaf-4124-b083-bf390e5482ff", sr.ID) + th.AssertEquals(t, 0, sr.PortRangeMax) + th.AssertEquals(t, 0, sr.PortRangeMin) + th.AssertEquals(t, "", sr.Protocol) + th.AssertEquals(t, "", sr.RemoteGroupID) + th.AssertEquals(t, "", sr.RemoteIPPrefix) + th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sr.SecGroupID) + th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sr.TenantID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/4ec89087-d057-4e2c-911f-60a3b47ee304", 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(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/results.go new file mode 100644 index 000000000000..6e1385768932 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/results.go @@ -0,0 +1,133 @@ +package rules + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// SecGroupRule represents a rule to dictate the behaviour of incoming or +// outgoing traffic for a particular security group. +type SecGroupRule struct { + // The UUID for this security group rule. + ID string + + // The direction in which the security group rule is applied. The only values + // allowed are "ingress" or "egress". For a compute instance, an ingress + // security group rule is applied to incoming (ingress) traffic for that + // instance. An egress rule is applied to traffic leaving the instance. + Direction string + + // Must be IPv4 or IPv6, and addresses represented in CIDR must match the + // ingress or egress rules. + EtherType string `json:"ethertype" mapstructure:"ethertype"` + + // The security group ID to associate with this security group rule. + SecGroupID string `json:"security_group_id" mapstructure:"security_group_id"` + + // The minimum port number in the range that is matched by the security group + // rule. If the protocol is TCP or UDP, this value must be less than or equal + // to the value of the PortRangeMax attribute. If the protocol is ICMP, this + // value must be an ICMP type. + PortRangeMin int `json:"port_range_min" mapstructure:"port_range_min"` + + // The maximum port number in the range that is matched by the security group + // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If + // the protocol is ICMP, this value must be an ICMP type. + PortRangeMax int `json:"port_range_max" mapstructure:"port_range_max"` + + // The protocol that is matched by the security group rule. Valid values are + // "tcp", "udp", "icmp" or an empty string. + Protocol string + + // The remote group ID to be associated with this security group rule. You + // can specify either RemoteGroupID or RemoteIPPrefix. + RemoteGroupID string `json:"remote_group_id" mapstructure:"remote_group_id"` + + // The remote IP prefix to be associated with this security group rule. You + // can specify either RemoteGroupID or RemoteIPPrefix . This attribute + // matches the specified IP prefix as the source IP address of the IP packet. + RemoteIPPrefix string `json:"remote_ip_prefix" mapstructure:"remote_ip_prefix"` + + // The owner of this security group rule. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` +} + +// SecGroupRulePage is the page returned by a pager when traversing over a +// collection of security group rules. +type SecGroupRulePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of security group rules has +// reached the end of a page and the pager seeks to traverse over a new one. In +// order to do this, it needs to construct the next page's URL. +func (p SecGroupRulePage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"security_group_rules_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a SecGroupRulePage struct is empty. +func (p SecGroupRulePage) IsEmpty() (bool, error) { + is, err := ExtractRules(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractRules accepts a Page struct, specifically a SecGroupRulePage struct, +// and extracts the elements into a slice of SecGroupRule structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRules(page pagination.Page) ([]SecGroupRule, error) { + var resp struct { + SecGroupRules []SecGroupRule `mapstructure:"security_group_rules" json:"security_group_rules"` + } + + err := mapstructure.Decode(page.(SecGroupRulePage).Body, &resp) + + return resp.SecGroupRules, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a security rule. +func (r commonResult) Extract() (*SecGroupRule, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + SecGroupRule *SecGroupRule `mapstructure:"security_group_rule" json:"security_group_rule"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.SecGroupRule, 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 +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go new file mode 100644 index 000000000000..8e2b2bb28d26 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go @@ -0,0 +1,13 @@ +package rules + +import "github.com/rackspace/gophercloud" + +const rootPath = "security-group-rules" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/doc.go new file mode 100644 index 000000000000..c87a7ce2708e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/doc.go @@ -0,0 +1,9 @@ +// Package networks contains functionality for working with Neutron network +// resources. A network is an isolated virtual layer-2 broadcast domain that is +// typically reserved for the tenant who created it (unless you configure the +// network to be shared). Tenants can create multiple networks until the +// thresholds per-tenant quota is reached. +// +// In the v2.0 Networking API, the network is the main entity. Ports and subnets +// are always associated with a network. +package networks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/errors.go new file mode 100644 index 000000000000..83c4a6a8683c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/errors.go @@ -0,0 +1 @@ +package networks 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 new file mode 100644 index 000000000000..7be322740068 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests.go @@ -0,0 +1,191 @@ +package networks + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +type networkOpts struct { + AdminStateUp *bool + Name string + Shared *bool + TenantID string +} + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToNetworkListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the network attributes you want to see returned. SortKey allows you to sort +// by a particular network attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + Shared *bool `q:"shared"` + ID string `q:"id"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToNetworkListQuery() (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 +// 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 ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToNetworkListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific network based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c, id), &res.Body, nil) + return res +} + +// 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 { + ToNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts networkOpts + +// ToNetworkCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + n := make(map[string]interface{}) + + if opts.AdminStateUp != nil { + n["admin_state_up"] = &opts.AdminStateUp + } + if opts.Name != "" { + n["name"] = opts.Name + } + if opts.Shared != nil { + n["shared"] = &opts.Shared + } + if opts.TenantID != "" { + n["tenant_id"] = opts.TenantID + } + + return map[string]interface{}{"network": n}, nil +} + +// 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 CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToNetworkCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Post(createURL(c), reqBody, &res.Body, nil) + return res +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update 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 UpdateOptsBuilder interface { + ToNetworkUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts networkOpts + +// ToNetworkUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) { + n := make(map[string]interface{}) + + if opts.AdminStateUp != nil { + n["admin_state_up"] = &opts.AdminStateUp + } + if opts.Name != "" { + n["name"] = opts.Name + } + if opts.Shared != nil { + n["shared"] = &opts.Shared + } + + return map[string]interface{}{"network": n}, nil +} + +// 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 UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToNetworkUpdateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = c.Put(updateURL(c, networkID), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + + return res +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(deleteURL(c, networkID), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go new file mode 100644 index 000000000000..a263b7b16b2c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/requests_test.go @@ -0,0 +1,275 @@ +package networks + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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, ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []Network{ + 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", + }, + 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("/v2.0/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("/v2.0/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 := 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("/v2.0/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 := 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("/v2.0/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, iFalse := true, false + options := UpdateOpts{Name: "new_network_name", AdminStateUp: &iFalse, 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("/v2.0/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/openstack/networking/v2/networks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/results.go new file mode 100644 index 000000000000..3ecedde9ac07 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/results.go @@ -0,0 +1,116 @@ +package networks + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*Network, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *Network `json:"network"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Network, 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 { + gophercloud.ErrResult +} + +// Network represents, well, a network. +type Network struct { + // UUID for the network + ID string `mapstructure:"id" json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `mapstructure:"name" json:"name"` + + // The administrative state of network. If false (down), the network does not forward packets. + AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `mapstructure:"status" json:"status"` + + // Subnets associated with this network. + Subnets []string `mapstructure:"subnets" json:"subnets"` + + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` + + // Specifies whether the network resource can be accessed by any tenant or not. + Shared bool `mapstructure:"shared" json:"shared"` +} + +// NetworkPage is the page returned by a pager when traversing over a +// collection of networks. +type NetworkPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of networks has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p NetworkPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"networks_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (p NetworkPage) IsEmpty() (bool, error) { + is, err := ExtractNetworks(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct, +// and extracts the elements into a slice of Network structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractNetworks(page pagination.Page) ([]Network, error) { + var resp struct { + Networks []Network `mapstructure:"networks" json:"networks"` + } + + err := mapstructure.Decode(page.(NetworkPage).Body, &resp) + + return resp.Networks, err +} 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 new file mode 100644 index 000000000000..a9eecc52956e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls.go @@ -0,0 +1,31 @@ +package networks + +import "github.com/rackspace/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("networks", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("networks") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +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/networks/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls_test.go new file mode 100644 index 000000000000..caf77dbe041a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/networks/urls_test.go @@ -0,0 +1,38 @@ +package networks + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "v2.0/networks/foo" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "v2.0/networks" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "v2.0/networks" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "v2.0/networks/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/doc.go new file mode 100644 index 000000000000..f16a4bb01bef --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/doc.go @@ -0,0 +1,8 @@ +// Package ports contains functionality for working with Neutron port resources. +// A port represents a virtual switch port on a logical network switch. Virtual +// instances attach their interfaces into ports. The logical port also defines +// the MAC address and the IP address(es) to be assigned to the interfaces +// plugged into them. When IP addresses are associated to a port, this also +// implies the port is associated with a subnet, as the IP address was taken +// from the allocation pool for a specific subnet. +package ports diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/errors.go new file mode 100644 index 000000000000..111d977e7490 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/errors.go @@ -0,0 +1,11 @@ +package ports + +import "fmt" + +func err(str string) error { + return fmt.Errorf("%s", str) +} + +var ( + errNetworkIDRequired = err("A Network ID is required") +) 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 new file mode 100644 index 000000000000..781a3c3e745a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests.go @@ -0,0 +1,225 @@ +package ports + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPortListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the port attributes you want to see returned. SortKey allows you to sort +// by a particular port attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + DeviceOwner string `q:"device_owner"` + MACAddress string `q:"mac_address"` + ID string `q:"id"` + DeviceID string `q:"device_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToPortListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPortListQuery() (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 +// 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 ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToPortListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PortPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific port based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c, id), &res.Body, nil) + return res +} + +// 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 { + ToPortCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new port. +type CreateOpts struct { + NetworkID string + Name string + AdminStateUp *bool + MACAddress string + FixedIPs interface{} + DeviceID string + DeviceOwner string + TenantID string + SecurityGroups []string +} + +// ToPortCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) { + p := make(map[string]interface{}) + + if opts.NetworkID == "" { + return nil, errNetworkIDRequired + } + p["network_id"] = opts.NetworkID + + if opts.DeviceID != "" { + p["device_id"] = opts.DeviceID + } + if opts.DeviceOwner != "" { + p["device_owner"] = opts.DeviceOwner + } + if opts.FixedIPs != nil { + p["fixed_ips"] = opts.FixedIPs + } + if opts.SecurityGroups != nil { + p["security_groups"] = opts.SecurityGroups + } + if opts.TenantID != "" { + p["tenant_id"] = opts.TenantID + } + if opts.AdminStateUp != nil { + p["admin_state_up"] = &opts.AdminStateUp + } + if opts.Name != "" { + p["name"] = opts.Name + } + if opts.MACAddress != "" { + p["mac_address"] = opts.MACAddress + } + + return map[string]interface{}{"port": p}, nil +} + +// 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 CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToPortCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Post(createURL(c), reqBody, &res.Body, nil) + return res +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update 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 UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing port. +type UpdateOpts struct { + Name string + AdminStateUp *bool + FixedIPs interface{} + DeviceID string + DeviceOwner string + SecurityGroups []string +} + +// ToPortUpdateMap casts an UpdateOpts struct to a map. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + p := make(map[string]interface{}) + + if opts.DeviceID != "" { + p["device_id"] = opts.DeviceID + } + if opts.DeviceOwner != "" { + p["device_owner"] = opts.DeviceOwner + } + if opts.FixedIPs != nil { + p["fixed_ips"] = opts.FixedIPs + } + if opts.SecurityGroups != nil { + p["security_groups"] = opts.SecurityGroups + } + if opts.AdminStateUp != nil { + p["admin_state_up"] = &opts.AdminStateUp + } + if opts.Name != "" { + p["name"] = opts.Name + } + + return map[string]interface{}{"port": p}, nil +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToPortUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Put(updateURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return res +} + +// Delete accepts a unique ID and deletes the port associated with it. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(deleteURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests_test.go new file mode 100644 index 000000000000..9e323efa3a99 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/requests_test.go @@ -0,0 +1,321 @@ +package ports + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractPorts(page) + if err != nil { + t.Errorf("Failed to extract subnets: %v", err) + return false, nil + } + + expected := []Port{ + 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: []IP{ + 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("/v2.0/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, []IP{ + 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("/v2.0/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 := CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []IP{ + 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, []IP{ + 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(), CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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 := UpdateOpts{ + Name: "new_port_name", + FixedIPs: []IP{ + 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, []IP{ + 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("/v2.0/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/openstack/networking/v2/ports/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go new file mode 100644 index 000000000000..2511ff53b213 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/results.go @@ -0,0 +1,126 @@ +package ports + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a port resource. +func (r commonResult) Extract() (*Port, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Port *Port `json:"port"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Port, 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 { + gophercloud.ErrResult +} + +// IP is a sub-struct that represents an individual IP. +type IP struct { + SubnetID string `mapstructure:"subnet_id" json:"subnet_id"` + IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"` +} + +// Port represents a Neutron port. See package documentation for a top-level +// description of what this is. +type Port struct { + // UUID for the port. + ID string `mapstructure:"id" json:"id"` + // Network that this port is associated with. + NetworkID string `mapstructure:"network_id" json:"network_id"` + // Human-readable name for the port. Might not be unique. + Name string `mapstructure:"name" json:"name"` + // Administrative state of port. If false (down), port does not forward packets. + AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"` + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values. + Status string `mapstructure:"status" json:"status"` + // Mac address to use on this port. + MACAddress string `mapstructure:"mac_address" json:"mac_address"` + // Specifies IP addresses for the port thus associating the port itself with + // the subnets where the IP addresses are picked from + FixedIPs []IP `mapstructure:"fixed_ips" json:"fixed_ips"` + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` + // Identifies the entity (e.g.: dhcp agent) using this port. + DeviceOwner string `mapstructure:"device_owner" json:"device_owner"` + // Specifies the IDs of any security groups associated with a port. + SecurityGroups []string `mapstructure:"security_groups" json:"security_groups"` + // Identifies the device (e.g., virtual server) using this port. + DeviceID string `mapstructure:"device_id" json:"device_id"` +} + +// PortPage is the page returned by a pager when traversing over a collection +// of network ports. +type PortPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of ports has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p PortPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"ports_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a PortPage struct is empty. +func (p PortPage) IsEmpty() (bool, error) { + is, err := ExtractPorts(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractPorts accepts a Page struct, specifically a PortPage struct, +// and extracts the elements into a slice of Port structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPorts(page pagination.Page) ([]Port, error) { + var resp struct { + Ports []Port `mapstructure:"ports" json:"ports"` + } + + err := mapstructure.Decode(page.(PortPage).Body, &resp) + + return resp.Ports, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls.go new file mode 100644 index 000000000000..6d0572f1fb27 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls.go @@ -0,0 +1,31 @@ +package ports + +import "github.com/rackspace/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("ports", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ports") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +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/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls_test.go new file mode 100644 index 000000000000..7fadd4dcb709 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/ports/urls_test.go @@ -0,0 +1,44 @@ +package ports + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"} +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "v2.0/ports" + th.AssertEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "v2.0/ports/foo" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "v2.0/ports" + th.AssertEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "v2.0/ports/foo" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "v2.0/ports/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/doc.go new file mode 100644 index 000000000000..43e8296c7f2e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/doc.go @@ -0,0 +1,10 @@ +// Package subnets contains functionality for working with Neutron subnet +// resources. A subnet represents an IP address block that can be used to +// assign IP addresses to virtual instances. Each subnet must have a CIDR and +// must be associated with a network. IPs can either be selected from the whole +// subnet CIDR or from allocation pools specified by the user. +// +// A subnet can also have a gateway, a list of DNS name servers, and host routes. +// This information is pushed to instances whose interfaces are associated with +// the subnet. +package subnets diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/errors.go new file mode 100644 index 000000000000..0db0a6e60477 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/errors.go @@ -0,0 +1,13 @@ +package subnets + +import "fmt" + +func err(str string) error { + return fmt.Errorf("%s", str) +} + +var ( + errNetworkIDRequired = err("A network ID is required") + errCIDRRequired = err("A valid CIDR is required") + errInvalidIPType = err("An IP type must either be 4 or 6") +) diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go new file mode 100644 index 000000000000..6e01f059d754 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests.go @@ -0,0 +1,236 @@ +package subnets + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// AdminState gives users a solid type to work with for create and update +// operations. It is recommended that users use the `Up` and `Down` enums. +type AdminState *bool + +// Convenience vars for AdminStateUp values. +var ( + iTrue = true + iFalse = false + + Up AdminState = &iTrue + Down AdminState = &iFalse +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSubnetListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the subnet attributes you want to see returned. SortKey allows you to sort +// by a particular subnet attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Name string `q:"name"` + EnableDHCP *bool `q:"enable_dhcp"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + IPVersion int `q:"ip_version"` + GatewayIP string `q:"gateway_ip"` + CIDR string `q:"cidr"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToSubnetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSubnetListQuery() (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 +// 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 ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSubnetListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SubnetPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific subnet based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c, id), &res.Body, nil) + return res +} + +// Valid IP types +const ( + IPv4 = 4 + IPv6 = 6 +) + +// 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 { + ToSubnetCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new subnet. +type CreateOpts struct { + // Required + NetworkID string + CIDR string + // Optional + Name string + TenantID string + AllocationPools []AllocationPool + GatewayIP string + IPVersion int + EnableDHCP *bool + DNSNameservers []string + HostRoutes []HostRoute +} + +// ToSubnetCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.NetworkID == "" { + return nil, errNetworkIDRequired + } + if opts.CIDR == "" { + return nil, errCIDRRequired + } + if opts.IPVersion != 0 && opts.IPVersion != IPv4 && opts.IPVersion != IPv6 { + return nil, errInvalidIPType + } + + s["network_id"] = opts.NetworkID + s["cidr"] = opts.CIDR + + if opts.EnableDHCP != nil { + s["enable_dhcp"] = &opts.EnableDHCP + } + if opts.Name != "" { + s["name"] = opts.Name + } + if opts.GatewayIP != "" { + s["gateway_ip"] = opts.GatewayIP + } + if opts.TenantID != "" { + s["tenant_id"] = opts.TenantID + } + if opts.IPVersion != 0 { + s["ip_version"] = opts.IPVersion + } + if len(opts.AllocationPools) != 0 { + s["allocation_pools"] = opts.AllocationPools + } + if len(opts.DNSNameservers) != 0 { + s["dns_nameservers"] = opts.DNSNameservers + } + if len(opts.HostRoutes) != 0 { + s["host_routes"] = opts.HostRoutes + } + + return map[string]interface{}{"subnet": s}, nil +} + +// 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 CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToSubnetCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Post(createURL(c), reqBody, &res.Body, nil) + return res +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSubnetUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing subnet. +type UpdateOpts struct { + Name string + GatewayIP string + DNSNameservers []string + HostRoutes []HostRoute + EnableDHCP *bool +} + +// ToSubnetUpdateMap casts an UpdateOpts struct to a map. +func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.EnableDHCP != nil { + s["enable_dhcp"] = &opts.EnableDHCP + } + if opts.Name != "" { + s["name"] = opts.Name + } + if opts.GatewayIP != "" { + s["gateway_ip"] = opts.GatewayIP + } + if len(opts.DNSNameservers) != 0 { + s["dns_nameservers"] = opts.DNSNameservers + } + if len(opts.HostRoutes) != 0 { + s["host_routes"] = opts.HostRoutes + } + + return map[string]interface{}{"subnet": s}, nil +} + +// Update accepts a UpdateOpts struct and updates an existing subnet using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToSubnetUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Put(updateURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + + return res +} + +// Delete accepts a unique ID and deletes the subnet associated with it. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(deleteURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests_test.go new file mode 100644 index 000000000000..987064ada642 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/requests_test.go @@ -0,0 +1,362 @@ +package subnets + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractSubnets(page) + if err != nil { + t.Errorf("Failed to extract subnets: %v", err) + return false, nil + } + + expected := []Subnet{ + Subnet{ + Name: "private-subnet", + EnableDHCP: true, + NetworkID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e", + DNSNameservers: []string{}, + AllocationPools: []AllocationPool{ + AllocationPool{ + Start: "10.0.0.2", + End: "10.0.0.254", + }, + }, + HostRoutes: []HostRoute{}, + IPVersion: 4, + GatewayIP: "10.0.0.1", + CIDR: "10.0.0.0/24", + ID: "08eae331-0402-425a-923c-34f7cfe39c1b", + }, + Subnet{ + Name: "my_subnet", + EnableDHCP: true, + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + TenantID: "4fd44f30292945e481c7b8a0c8908869", + DNSNameservers: []string{}, + AllocationPools: []AllocationPool{ + AllocationPool{ + Start: "192.0.0.2", + End: "192.255.255.254", + }, + }, + HostRoutes: []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("/v2.0/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, []AllocationPool{ + AllocationPool{ + Start: "192.0.0.2", + End: "192.255.255.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []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("/v2.0/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 := CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + IPVersion: 4, + CIDR: "192.168.199.0/24", + AllocationPools: []AllocationPool{ + AllocationPool{ + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }, + DNSNameservers: []string{"foo"}, + HostRoutes: []HostRoute{ + 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, []AllocationPool{ + AllocationPool{ + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }) + th.AssertDeepEquals(t, s.HostRoutes, []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(), CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + res = Create(fake.ServiceClient(), 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("/v2.0/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 := UpdateOpts{ + Name: "my_new_subnet", + DNSNameservers: []string{"foo"}, + HostRoutes: []HostRoute{ + 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("/v2.0/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/openstack/networking/v2/subnets/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go new file mode 100644 index 000000000000..1910f17dd96f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/results.go @@ -0,0 +1,132 @@ +package subnets + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a subnet resource. +func (r commonResult) Extract() (*Subnet, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Subnet *Subnet `json:"subnet"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Subnet, 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 { + gophercloud.ErrResult +} + +// AllocationPool represents a sub-range of cidr available for dynamic +// allocation to ports, e.g. {Start: "10.0.0.2", End: "10.0.0.254"} +type AllocationPool struct { + Start string `json:"start"` + End string `json:"end"` +} + +// HostRoute represents a route that should be used by devices with IPs from +// a subnet (not including local subnet route). +type HostRoute struct { + DestinationCIDR string `json:"destination"` + NextHop string `json:"nexthop"` +} + +// Subnet represents a subnet. See package documentation for a top-level +// description of what this is. +type Subnet struct { + // UUID representing the subnet + ID string `mapstructure:"id" json:"id"` + // UUID of the parent network + NetworkID string `mapstructure:"network_id" json:"network_id"` + // Human-readable name for the subnet. Might not be unique. + Name string `mapstructure:"name" json:"name"` + // IP version, either `4' or `6' + IPVersion int `mapstructure:"ip_version" json:"ip_version"` + // CIDR representing IP range for this subnet, based on IP version + CIDR string `mapstructure:"cidr" json:"cidr"` + // Default gateway used by devices in this subnet + GatewayIP string `mapstructure:"gateway_ip" json:"gateway_ip"` + // DNS name servers used by hosts in this subnet. + DNSNameservers []string `mapstructure:"dns_nameservers" json:"dns_nameservers"` + // Sub-ranges of CIDR available for dynamic allocation to ports. See AllocationPool. + AllocationPools []AllocationPool `mapstructure:"allocation_pools" json:"allocation_pools"` + // Routes that should be used by devices with IPs from this subnet (not including local subnet route). + HostRoutes []HostRoute `mapstructure:"host_routes" json:"host_routes"` + // Specifies whether DHCP is enabled for this subnet or not. + EnableDHCP bool `mapstructure:"enable_dhcp" json:"enable_dhcp"` + // Owner of network. Only admin users can specify a tenant_id other than its own. + TenantID string `mapstructure:"tenant_id" json:"tenant_id"` +} + +// SubnetPage is the page returned by a pager when traversing over a collection +// of subnets. +type SubnetPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of subnets has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p SubnetPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"subnets_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a SubnetPage struct is empty. +func (p SubnetPage) IsEmpty() (bool, error) { + is, err := ExtractSubnets(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractSubnets accepts a Page struct, specifically a SubnetPage struct, +// and extracts the elements into a slice of Subnet structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractSubnets(page pagination.Page) ([]Subnet, error) { + var resp struct { + Subnets []Subnet `mapstructure:"subnets" json:"subnets"` + } + + err := mapstructure.Decode(page.(SubnetPage).Body, &resp) + + return resp.Subnets, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls.go new file mode 100644 index 000000000000..0d0236894158 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls.go @@ -0,0 +1,31 @@ +package subnets + +import "github.com/rackspace/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("subnets", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("subnets") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +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/subnets/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls_test.go new file mode 100644 index 000000000000..aeeddf35495c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/networking/v2/subnets/urls_test.go @@ -0,0 +1,44 @@ +package subnets + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"} +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + "v2.0/subnets" + th.AssertEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "v2.0/subnets/foo" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "v2.0/subnets" + th.AssertEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "v2.0/subnets/foo" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "v2.0/subnets/foo" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/doc.go new file mode 100644 index 000000000000..f5f894a9e566 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/doc.go @@ -0,0 +1,8 @@ +// Package accounts contains functionality for working with Object Storage +// account resources. An account is the top-level resource the object storage +// hierarchy: containers belong to accounts, objects belong to containers. +// +// Another way of thinking of an account is like a namespace for all your +// resources. It is synonymous with a project or tenant in other OpenStack +// services. +package accounts 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 new file mode 100644 index 000000000000..f22b68700538 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/fixtures.go @@ -0,0 +1,38 @@ +// +build fixtures + +package accounts + +import ( + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// HandleGetAccountSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `Get` response. +func HandleGetAccountSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "HEAD") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Set("X-Account-Container-Count", "2") + w.Header().Set("X-Account-Bytes-Used", "14") + w.Header().Set("X-Account-Meta-Subject", "books") + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleUpdateAccountSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateAccountSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/", 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, "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.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go new file mode 100644 index 000000000000..a6451157050f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests.go @@ -0,0 +1,103 @@ +package accounts + +import "github.com/rackspace/gophercloud" + +// GetOptsBuilder allows extensions to add additional headers to the Get +// request. +type GetOptsBuilder interface { + ToAccountGetMap() (map[string]string, error) +} + +// GetOpts is a structure that contains parameters for getting an account's +// metadata. +type GetOpts struct { + Newest bool `h:"X-Newest"` +} + +// ToAccountGetMap formats a GetOpts into a map[string]string of headers. +func (opts GetOpts) ToAccountGetMap() (map[string]string, error) { + return gophercloud.BuildHeaders(opts) +} + +// Get is a function that retrieves an account's metadata. To extract just the +// custom metadata, call the ExtractMetadata method on the GetResult. To extract +// all the headers that are returned (including the metadata), call the +// ExtractHeader method on the GetResult. +func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) GetResult { + var res GetResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToAccountGetMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + resp, err := c.Request("HEAD", getURL(c), gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{204}, + }) + res.Header = resp.Header + res.Err = err + return res +} + +// UpdateOptsBuilder allows extensions to add additional headers to the Update +// request. +type UpdateOptsBuilder interface { + ToAccountUpdateMap() (map[string]string, error) +} + +// UpdateOpts is a structure that contains parameters for updating, creating, or +// deleting an account's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + TempURLKey string `h:"X-Account-Meta-Temp-URL-Key"` + TempURLKey2 string `h:"X-Account-Meta-Temp-URL-Key-2"` +} + +// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers. +func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) { + headers, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + headers["X-Account-Meta-"+k] = v + } + return headers, err +} + +// Update is a function that creates, updates, or deletes an account's metadata. +// To extract the headers returned, call the Extract method on the UpdateResult. +func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + h := make(map[string]string) + + if opts != nil { + headers, err := opts.ToAccountUpdateMap() + if err != nil { + res.Err = err + return res + } + for k, v := range headers { + h[k] = v + } + } + + resp, err := c.Request("POST", updateURL(c), gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201, 202, 204}, + }) + res.Header = resp.Header + res.Err = err + return res +} 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 new file mode 100644 index 000000000000..6454c0ac4ca7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/requests_test.go @@ -0,0 +1,32 @@ +package accounts + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestUpdateAccount(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateAccountSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}} + res := Update(fake.ServiceClient(), options) + th.AssertNoErr(t, res.Err) +} + +func TestGetAccount(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetAccountSuccessfully(t) + + 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 new file mode 100644 index 000000000000..6ab1a2306174 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/results.go @@ -0,0 +1,102 @@ +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) { + if gr.Err != nil { + return nil, gr.Err + } + + metadata := make(map[string]string) + for k, v := range gr.Header { + if strings.HasPrefix(k, "X-Account-Meta-") { + key := strings.TrimPrefix(k, "X-Account-Meta-") + metadata[key] = v[0] + } + } + return metadata, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls.go new file mode 100644 index 000000000000..9952fe43451c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls.go @@ -0,0 +1,11 @@ +package accounts + +import "github.com/rackspace/gophercloud" + +func getURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} + +func updateURL(c *gophercloud.ServiceClient) string { + return getURL(c) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls_test.go new file mode 100644 index 000000000000..074d52dfd5c2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts/urls_test.go @@ -0,0 +1,26 @@ +package accounts + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient()) + expected := endpoint + th.CheckEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient()) + expected := endpoint + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/doc.go new file mode 100644 index 000000000000..5fed5537f136 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/doc.go @@ -0,0 +1,8 @@ +// Package containers contains functionality for working with Object Storage +// container resources. A container serves as a logical namespace for objects +// that are placed inside it - an object with the same name in two different +// containers represents two different objects. +// +// In addition to containing objects, you can also use the container to control +// access to objects by using an access control list (ACL). +package containers 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 new file mode 100644 index 000000000000..e60735248fed --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/fixtures.go @@ -0,0 +1,143 @@ +// +build fixtures + +package containers + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// ExpectedListInfo is the result expected from a call to `List` when full +// info is requested. +var ExpectedListInfo = []Container{ + Container{ + Count: 0, + Bytes: 0, + Name: "janeausten", + }, + Container{ + Count: 1, + Bytes: 14, + Name: "marktwain", + }, +} + +// ExpectedListNames is the result expected from a call to `List` when just +// container names are requested. +var ExpectedListNames = []string{"janeausten", "marktwain"} + +// HandleListContainerInfoSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `List` response when full info is requested. +func HandleListContainerInfoSuccessfully(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.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, `[ + { + "count": 0, + "bytes": 0, + "name": "janeausten" + }, + { + "count": 1, + "bytes": 14, + "name": "marktwain" + } + ]`) + case "janeausten": + fmt.Fprintf(w, `[ + { + "count": 1, + "bytes": 14, + "name": "marktwain" + } + ]`) + case "marktwain": + fmt.Fprintf(w, `[]`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleListContainerNamesSuccessfully creates an HTTP handler at `/` on the test handler mux that +// responds with a `ListNames` response when only container names are requested. +func HandleListContainerNamesSuccessfully(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", "text/plain") + + w.Header().Set("Content-Type", "text/plain") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, "janeausten\nmarktwain\n") + case "janeausten": + fmt.Fprintf(w, "marktwain\n") + case "marktwain": + fmt.Fprintf(w, ``) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleCreateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Create` response. +func HandleCreateContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", 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") + + w.Header().Add("X-Container-Meta-Foo", "bar") + w.Header().Add("X-Trans-Id", "1234567") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleDeleteContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Delete` response. +func HandleDeleteContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleUpdateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleGetContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `Get` response. +func HandleGetContainerSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "HEAD") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go new file mode 100644 index 000000000000..bbf8cdb952cc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests.go @@ -0,0 +1,199 @@ +package containers + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToContainerListParams() (bool, string, error) +} + +// ListOpts is a structure that holds options for listing containers. +type ListOpts struct { + Full bool + Limit int `q:"limit"` + Marker string `q:"marker"` + EndMarker string `q:"end_marker"` + Format string `q:"format"` + Prefix string `q:"prefix"` + Delimiter string `q:"delimiter"` +} + +// ToContainerListParams formats a ListOpts into a query string and boolean +// representing whether to list complete information for each container. +func (opts ListOpts) ToContainerListParams() (bool, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return false, "", err + } + return opts.Full, q.String(), nil +} + +// List is a function that retrieves containers associated with the account as +// well as account metadata. It returns a pager which can be iterated with the +// EachPage function. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"} + + url := listURL(c) + if opts != nil { + full, query, err := opts.ToContainerListParams() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + + if full { + headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"} + } + } + + createPage := func(r pagination.PageResult) pagination.Page { + p := ContainerPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + pager := pagination.NewPager(c, url, createPage) + pager.Headers = headers + return pager +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToContainerCreateMap() (map[string]string, error) +} + +// CreateOpts is a structure that holds parameters for creating a container. +type CreateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerSyncTo string `h:"X-Container-Sync-To"` + ContainerSyncKey string `h:"X-Container-Sync-Key"` + ContainerWrite string `h:"X-Container-Write"` + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + IfNoneMatch string `h:"If-None-Match"` + VersionsLocation string `h:"X-Versions-Location"` +} + +// ToContainerCreateMap formats a CreateOpts into a map of headers. +func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Container-Meta-"+k] = v + } + return h, nil +} + +// Create is a function that creates a new container. +func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsBuilder) CreateResult { + var res CreateResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToContainerCreateMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + resp, err := c.Request("PUT", createURL(c, containerName), gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201, 202, 204}, + }) + res.Header = resp.Header + res.Err = err + return res +} + +// Delete is a function that deletes a container. +func Delete(c *gophercloud.ServiceClient, containerName string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(deleteURL(c, containerName), nil) + return res +} + +// 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 { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerSyncTo string `h:"X-Container-Sync-To"` + ContainerSyncKey string `h:"X-Container-Sync-Key"` + ContainerWrite string `h:"X-Container-Write"` + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + RemoveVersionsLocation string `h:"X-Remove-Versions-Location"` + VersionsLocation string `h:"X-Versions-Location"` +} + +// 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 + } + for k, v := range opts.Metadata { + h["X-Container-Meta-"+k] = v + } + 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 := c.Request("POST", updateURL(c, containerName), gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201, 202, 204}, + }) + res.Header = resp.Header + 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 := c.Request("HEAD", getURL(c, containerName), gophercloud.RequestOpts{ + OkCodes: []int{200, 204}, + }) + res.Header = resp.Header + res.Err = err + return res +} 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 new file mode 100644 index 000000000000..0ccd5a778681 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/requests_test.go @@ -0,0 +1,117 @@ +package containers + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +var metadata = map[string]string{"gophercloud-test": "containers"} + +func TestListContainerInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerInfoSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListAllContainerInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerInfoSuccessfully(t) + + allPages, err := List(fake.ServiceClient(), &ListOpts{Full: true}).AllPages() + th.AssertNoErr(t, err) + actual, err := ExtractInfo(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedListInfo, actual) +} + +func TestListContainerNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerNamesSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListAllContainerNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListContainerNamesSuccessfully(t) + + allPages, err := List(fake.ServiceClient(), &ListOpts{Full: false}).AllPages() + th.AssertNoErr(t, err) + actual, err := ExtractNames(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedListNames, actual) +} + +func TestCreateContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateContainerSuccessfully(t) + + options := CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}} + res := Create(fake.ServiceClient(), "testContainer", options) + 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) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteContainerSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer") + th.CheckNoErr(t, res.Err) +} + +func TestUpateContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateContainerSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"foo": "bar"}} + res := Update(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) +} + +func TestGetContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetContainerSuccessfully(t) + + _, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata() + th.CheckNoErr(t, err) +} 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 new file mode 100644 index 000000000000..e682b8dccbfb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/results.go @@ -0,0 +1,270 @@ +package containers + +import ( + "fmt" + "strings" + "time" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Container represents a container resource. +type Container struct { + // The total number of bytes stored in the container. + Bytes int `json:"bytes" mapstructure:"bytes"` + + // The total number of objects stored in the container. + Count int `json:"count" mapstructure:"count"` + + // The name of the container. + Name string `json:"name" mapstructure:"name"` +} + +// ContainerPage is the page returned by a pager when traversing over a +// collection of containers. +type ContainerPage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a ListResult contains no container names. +func (r ContainerPage) IsEmpty() (bool, error) { + names, err := ExtractNames(r) + if err != nil { + return true, err + } + return len(names) == 0, nil +} + +// LastMarker returns the last container name in a ListResult. +func (r ContainerPage) LastMarker() (string, error) { + names, err := ExtractNames(r) + if err != nil { + return "", err + } + if len(names) == 0 { + return "", nil + } + return names[len(names)-1], nil +} + +// ExtractInfo is a function that takes a ListResult and returns the containers' information. +func ExtractInfo(page pagination.Page) ([]Container, error) { + untyped := page.(ContainerPage).Body.([]interface{}) + results := make([]Container, len(untyped)) + for index, each := range untyped { + container := each.(map[string]interface{}) + err := mapstructure.Decode(container, &results[index]) + if err != nil { + return results, err + } + } + return results, nil +} + +// ExtractNames is a function that takes a ListResult and returns the containers' names. +func ExtractNames(page pagination.Page) ([]string, error) { + casted := page.(ContainerPage) + ct := casted.Header.Get("Content-Type") + + switch { + case strings.HasPrefix(ct, "application/json"): + parsed, err := ExtractInfo(page) + if err != nil { + return nil, err + } + + names := make([]string, 0, len(parsed)) + for _, container := range parsed { + names = append(names, container.Name) + } + return names, nil + case strings.HasPrefix(ct, "text/plain"): + names := make([]string, 0, 50) + + body := string(page.(ContainerPage).Body.([]uint8)) + for _, name := range strings.Split(body, "\n") { + if len(name) > 0 { + names = append(names, name) + } + } + + return names, nil + default: + return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct) + } +} + +// 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) { + 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 +} + +// 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. +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. +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/containers/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls.go new file mode 100644 index 000000000000..f864f846eb26 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls.go @@ -0,0 +1,23 @@ +package containers + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} + +func createURL(c *gophercloud.ServiceClient, container string) string { + return c.ServiceURL(container) +} + +func getURL(c *gophercloud.ServiceClient, container string) string { + return createURL(c, container) +} + +func deleteURL(c *gophercloud.ServiceClient, container string) string { + return createURL(c, container) +} + +func updateURL(c *gophercloud.ServiceClient, container string) string { + return createURL(c, container) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls_test.go new file mode 100644 index 000000000000..d043a2aae50d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers/urls_test.go @@ -0,0 +1,43 @@ +package containers + +import ( + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" + "testing" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient()) + expected := endpoint + th.CheckEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/doc.go new file mode 100644 index 000000000000..30a9adde1cad --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/doc.go @@ -0,0 +1,5 @@ +// Package objects contains functionality for working with Object Storage +// object resources. An object is a resource that represents and contains data +// - such as documents, images, and so on. You can also store custom metadata +// with an object. +package objects 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 new file mode 100644 index 000000000000..ec616371a3c5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/fixtures.go @@ -0,0 +1,182 @@ +// +build fixtures + +package objects + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// HandleDownloadObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Download` response. +func HandleDownloadObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", 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, "Successful download with Gophercloud") + }) +} + +// ExpectedListInfo is the result expected from a call to `List` when full +// info is requested. +var ExpectedListInfo = []Object{ + Object{ + Hash: "451e372e48e0f6b1114fa0724aa79fa1", + LastModified: "2009-11-10 23:00:00 +0000 UTC", + Bytes: 14, + Name: "goodbye", + ContentType: "application/octet-stream", + }, + Object{ + Hash: "451e372e48e0f6b1114fa0724aa79fa1", + LastModified: "2009-11-10 23:00:00 +0000 UTC", + Bytes: 14, + Name: "hello", + ContentType: "application/octet-stream", + }, +} + +// ExpectedListNames is the result expected from a call to `List` when just +// object names are requested. +var ExpectedListNames = []string{"hello", "goodbye"} + +// HandleListObjectsInfoSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `List` response when full info is requested. +func HandleListObjectsInfoSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, `[ + { + "hash": "451e372e48e0f6b1114fa0724aa79fa1", + "last_modified": "2009-11-10 23:00:00 +0000 UTC", + "bytes": 14, + "name": "goodbye", + "content_type": "application/octet-stream" + }, + { + "hash": "451e372e48e0f6b1114fa0724aa79fa1", + "last_modified": "2009-11-10 23:00:00 +0000 UTC", + "bytes": 14, + "name": "hello", + "content_type": "application/octet-stream" + } + ]`) + case "hello": + fmt.Fprintf(w, `[]`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// HandleListObjectNamesSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with a `List` response when only object names are requested. +func HandleListObjectNamesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer", 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", "text/plain") + + w.Header().Set("Content-Type", "text/plain") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, "hello\ngoodbye\n") + case "goodbye": + fmt.Fprintf(w, "") + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// 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) + }) +} + +// HandleCopyObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Copy` response. +func HandleCopyObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "COPY") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "Destination", "/newTestContainer/newTestObject") + w.WriteHeader(http.StatusCreated) + }) +} + +// HandleDeleteObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Delete` response. +func HandleDeleteObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleUpdateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Update` response. +func HandleUpdateObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Object-Meta-Gophercloud-Test", "objects") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleGetObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that +// responds with a `Get` response. +func HandleGetObjectSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "HEAD") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("X-Object-Meta-Gophercloud-Test", "objects") + w.WriteHeader(http.StatusNoContent) + }) +} 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 new file mode 100644 index 000000000000..7eedde25509f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests.go @@ -0,0 +1,464 @@ +package objects + +import ( + "crypto/hmac" + "crypto/sha1" + "fmt" + "io" + "strings" + "time" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToObjectListParams() (bool, string, error) +} + +// 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"` + EndMarker string `q:"end_marker"` + Format string `q:"format"` + Prefix string `q:"prefix"` + Delimiter string `q:"delimiter"` + Path string `q:"path"` +} + +// ToObjectListParams formats a ListOpts into a query string and boolean +// representing whether to list complete information for each object. +func (opts ListOpts) ToObjectListParams() (bool, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return false, "", err + } + return opts.Full, q.String(), nil +} + +// List is a function that retrieves all objects in a container. It also returns the details +// for the container. To extract only the object information or names, pass the ListResult +// response to the ExtractInfo or ExtractNames function, respectively. +func List(c *gophercloud.ServiceClient, containerName string, opts ListOptsBuilder) pagination.Pager { + headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"} + + url := listURL(c, containerName) + if opts != nil { + full, query, err := opts.ToObjectListParams() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + + if full { + headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"} + } + } + + createPage := func(r pagination.PageResult) pagination.Page { + p := ObjectPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + pager := pagination.NewPager(c, url, createPage) + pager.Headers = headers + return pager +} + +// DownloadOptsBuilder allows extensions to add additional parameters to the +// Download request. +type DownloadOptsBuilder interface { + ToObjectDownloadParams() (map[string]string, string, error) +} + +// DownloadOpts is a structure that holds parameters for downloading an object. +type DownloadOpts struct { + IfMatch string `h:"If-Match"` + IfModifiedSince time.Time `h:"If-Modified-Since"` + IfNoneMatch string `h:"If-None-Match"` + IfUnmodifiedSince time.Time `h:"If-Unmodified-Since"` + Range string `h:"Range"` + Expires string `q:"expires"` + MultipartManifest string `q:"multipart-manifest"` + Signature string `q:"signature"` +} + +// ToObjectDownloadParams formats a DownloadOpts into a query string and map of +// headers. +func (opts DownloadOpts) ToObjectDownloadParams() (map[string]string, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return nil, "", err + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, q.String(), err + } + return h, q.String(), nil +} + +// Download is a function that retrieves the content and metadata for an object. +// To extract just the content, pass the DownloadResult response to the +// ExtractContent function. +func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOptsBuilder) DownloadResult { + var res DownloadResult + + url := downloadURL(c, containerName, objectName) + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, query, err := opts.ToObjectDownloadParams() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + + url += query + } + + resp, err := c.Request("GET", url, gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200, 304}, + }) + + res.Body = resp.Body + res.Err = err + res.Header = resp.Header + + return res +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToObjectCreateParams() (map[string]string, string, error) +} + +// CreateOpts is a structure that holds parameters for creating an object. +type CreateOpts struct { + Metadata map[string]string + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentLength int64 `h:"Content-Length"` + ContentType string `h:"Content-Type"` + CopyFrom string `h:"X-Copy-From"` + DeleteAfter int `h:"X-Delete-After"` + DeleteAt int `h:"X-Delete-At"` + DetectContentType string `h:"X-Detect-Content-Type"` + ETag string `h:"ETag"` + IfNoneMatch string `h:"If-None-Match"` + ObjectManifest string `h:"X-Object-Manifest"` + TransferEncoding string `h:"Transfer-Encoding"` + Expires string `q:"expires"` + MultipartManifest string `q:"multiple-manifest"` + Signature string `q:"signature"` +} + +// ToObjectCreateParams formats a CreateOpts into a query string and map of +// headers. +func (opts CreateOpts) ToObjectCreateParams() (map[string]string, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return nil, "", err + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, q.String(), err + } + + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + + return h, q.String(), nil +} + +// Create is a function that creates a new object or replaces an existing object. +func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + url := createURL(c, containerName, objectName) + h := make(map[string]string) + + if opts != nil { + headers, query, err := opts.ToObjectCreateParams() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + + url += query + } + + ropts := gophercloud.RequestOpts{ + RawBody: content, + MoreHeaders: h, + } + + resp, err := c.Request("PUT", url, ropts) + res.Header = resp.Header + res.Err = err + return res +} + +// CopyOptsBuilder allows extensions to add additional parameters to the +// Copy request. +type CopyOptsBuilder interface { + ToObjectCopyMap() (map[string]string, error) +} + +// CopyOpts is a structure that holds parameters for copying one object to +// another. +type CopyOpts struct { + Metadata map[string]string + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentType string `h:"Content-Type"` + Destination string `h:"Destination,required"` +} + +// ToObjectCopyMap formats a CopyOpts into a map of headers. +func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) { + if opts.Destination == "" { + return nil, fmt.Errorf("Required CopyOpts field 'Destination' not set.") + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + return h, nil +} + +// Copy is a function that copies one object to another. +func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) CopyResult { + var res CopyResult + h := c.AuthenticatedHeaders() + + headers, err := opts.ToObjectCopyMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + + url := copyURL(c, containerName, objectName) + resp, err := c.Request("COPY", url, gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + }) + res.Header = resp.Header + res.Err = err + return res +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToObjectDeleteQuery() (string, error) +} + +// DeleteOpts is a structure that holds parameters for deleting an object. +type DeleteOpts struct { + MultipartManifest string `q:"multipart-manifest"` +} + +// ToObjectDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToObjectDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// Delete is a function that deletes an object. +func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) DeleteResult { + var res DeleteResult + url := deleteURL(c, containerName, objectName) + + if opts != nil { + query, err := opts.ToObjectDeleteQuery() + if err != nil { + res.Err = err + return res + } + url += query + } + + resp, err := c.Delete(url, nil) + res.Header = resp.Header + res.Err = err + return res +} + +// GetOptsBuilder allows extensions to add additional parameters to the +// Get request. +type GetOptsBuilder interface { + ToObjectGetQuery() (string, error) +} + +// GetOpts is a structure that holds parameters for getting an object's metadata. +type GetOpts struct { + Expires string `q:"expires"` + Signature string `q:"signature"` +} + +// ToObjectGetQuery formats a GetOpts into a query string. +func (opts GetOpts) ToObjectGetQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// Get is a function that retrieves the metadata of an object. To extract just the custom +// metadata, pass the GetResult response to the ExtractMetadata function. +func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOptsBuilder) GetResult { + var res GetResult + url := getURL(c, containerName, objectName) + + if opts != nil { + query, err := opts.ToObjectGetQuery() + if err != nil { + res.Err = err + return res + } + url += query + } + + resp, err := c.Request("HEAD", url, gophercloud.RequestOpts{ + OkCodes: []int{200, 204}, + }) + res.Header = resp.Header + res.Err = err + return res +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToObjectUpdateMap() (map[string]string, error) +} + +// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an +// object's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentType string `h:"Content-Type"` + DeleteAfter int `h:"X-Delete-After"` + DeleteAt int `h:"X-Delete-At"` + DetectContentType bool `h:"X-Detect-Content-Type"` +} + +// ToObjectUpdateMap formats a UpdateOpts into a map of headers. +func (opts UpdateOpts) ToObjectUpdateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + return h, nil +} + +// Update is a function that creates, updates, or deletes an object's metadata. +func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToObjectUpdateMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + url := updateURL(c, containerName, objectName) + resp, err := c.Request("POST", url, gophercloud.RequestOpts{ + MoreHeaders: h, + }) + res.Header = resp.Header + 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 new file mode 100644 index 000000000000..6be3534205ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/requests_test.go @@ -0,0 +1,142 @@ +package objects + +import ( + "bytes" + "io" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDownloadReader(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDownloadObjectSuccessfully(t) + + response := Download(fake.ServiceClient(), "testContainer", "testObject", nil) + defer response.Body.Close() + + // Check reader + buf := bytes.NewBuffer(make([]byte, 0)) + io.CopyN(buf, response.Body, 10) + th.CheckEquals(t, "Successful", string(buf.Bytes())) +} + +func TestDownloadExtraction(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDownloadObjectSuccessfully(t) + + response := Download(fake.ServiceClient(), "testContainer", "testObject", nil) + + // Check []byte extraction + bytes, err := response.ExtractContent() + th.AssertNoErr(t, err) + th.CheckEquals(t, "Successful download with Gophercloud", string(bytes)) +} + +func TestListObjectInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListObjectsInfoSuccessfully(t) + + count := 0 + options := &ListOpts{Full: true} + err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListObjectNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListObjectNamesSuccessfully(t) + + count := 0 + options := &ListOpts{Full: false} + err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateTextObjectSuccessfully(t) + + content := bytes.NewBufferString("Did gyre and gimble in the wabe") + 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() + HandleCopyObjectSuccessfully(t) + + options := &CopyOpts{Destination: "/newTestContainer/newTestObject"} + res := Copy(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteObjectSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil) + th.AssertNoErr(t, res.Err) +} + +func TestUpateObjectMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateObjectSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}} + res := Update(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestGetObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetObjectSuccessfully(t) + + expected := map[string]string{"Gophercloud-Test": "objects"} + actual, err := Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} 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 new file mode 100644 index 000000000000..ecb2c54582fe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/results.go @@ -0,0 +1,438 @@ +package objects + +import ( + "fmt" + "io" + "io/ioutil" + "strconv" + "strings" + "time" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Object is a structure that holds information related to a storage object. +type Object struct { + // 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 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 +// List function. +type ObjectPage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a ListResult contains no object names. +func (r ObjectPage) IsEmpty() (bool, error) { + names, err := ExtractNames(r) + if err != nil { + return true, err + } + return len(names) == 0, nil +} + +// LastMarker returns the last object name in a ListResult. +func (r ObjectPage) LastMarker() (string, error) { + names, err := ExtractNames(r) + if err != nil { + return "", err + } + if len(names) == 0 { + return "", nil + } + return names[len(names)-1], nil +} + +// ExtractInfo is a function that takes a page of objects and returns their full information. +func ExtractInfo(page pagination.Page) ([]Object, error) { + untyped := page.(ObjectPage).Body.([]interface{}) + results := make([]Object, len(untyped)) + for index, each := range untyped { + object := each.(map[string]interface{}) + err := mapstructure.Decode(object, &results[index]) + if err != nil { + return results, err + } + } + return results, nil +} + +// ExtractNames is a function that takes a page of objects and returns only their names. +func ExtractNames(page pagination.Page) ([]string, error) { + casted := page.(ObjectPage) + ct := casted.Header.Get("Content-Type") + switch { + case strings.HasPrefix(ct, "application/json"): + parsed, err := ExtractInfo(page) + if err != nil { + return nil, err + } + + names := make([]string, 0, len(parsed)) + for _, object := range parsed { + names = append(names, object.Name) + } + + return names, nil + case strings.HasPrefix(ct, "text/plain"): + names := make([]string, 0, 50) + + body := string(page.(ObjectPage).Body.([]uint8)) + for _, name := range strings.Split(body, "\n") { + if len(name) > 0 { + names = append(names, name) + } + } + + return names, nil + case strings.HasPrefix(ct, "text/html"): + return []string{}, nil + default: + return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct) + } +} + +// 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 +// once and not rewound. You can recreate a reader from the output of this +// function by using bytes.NewReader(downloadBytes) +func (dr DownloadResult) ExtractContent() ([]byte, error) { + if dr.Err != nil { + return nil, dr.Err + } + body, err := ioutil.ReadAll(dr.Body) + if err != nil { + return nil, err + } + dr.Body.Close() + 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) { + if gr.Err != nil { + return nil, gr.Err + } + metadata := make(map[string]string) + for k, v := range gr.Header { + if strings.HasPrefix(k, "X-Object-Meta-") { + key := strings.TrimPrefix(k, "X-Object-Meta-") + metadata[key] = v[0] + } + } + 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/openstack/objectstorage/v1/objects/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls.go new file mode 100644 index 000000000000..d2ec62cff22f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls.go @@ -0,0 +1,33 @@ +package objects + +import ( + "github.com/rackspace/gophercloud" +) + +func listURL(c *gophercloud.ServiceClient, container string) string { + return c.ServiceURL(container) +} + +func copyURL(c *gophercloud.ServiceClient, container, object string) string { + return c.ServiceURL(container, object) +} + +func createURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func getURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func deleteURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func downloadURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} + +func updateURL(c *gophercloud.ServiceClient, container, object string) string { + return copyURL(c, container, object) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls_test.go new file mode 100644 index 000000000000..1dcfe3543c79 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects/urls_test.go @@ -0,0 +1,56 @@ +package objects + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestListURL(t *testing.T) { + actual := listURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} + +func TestCopyURL(t *testing.T) { + actual := copyURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} + +func TestDownloadURL(t *testing.T) { + actual := downloadURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo", "bar") + expected := endpoint + "foo/bar" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/doc.go new file mode 100644 index 000000000000..f2db622d1fc2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/doc.go @@ -0,0 +1,4 @@ +// Package apiversions provides information and interaction with the different +// API versions for the OpenStack Heat service. This functionality is not +// restricted to this particular version. +package apiversions diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/requests.go new file mode 100644 index 000000000000..f6454c860977 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/requests.go @@ -0,0 +1,13 @@ +package apiversions + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListVersions lists all the Neutron API versions available to end-users +func ListVersions(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, apiVersionsURL(c), func(r pagination.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/requests_test.go new file mode 100644 index 000000000000..a2fc980d3588 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/requests_test.go @@ -0,0 +1,89 @@ +package apiversions + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", 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, ` +{ + "versions": [ + { + "status": "CURRENT", + "id": "v1.0", + "links": [ + { + "href": "http://23.253.228.211:8000/v1", + "rel": "self" + } + ] + } + ] +}`) + }) + + count := 0 + + ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractAPIVersions(page) + if err != nil { + t.Errorf("Failed to extract API versions: %v", err) + return false, err + } + + expected := []APIVersion{ + APIVersion{ + Status: "CURRENT", + ID: "v1.0", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://23.253.228.211:8000/v1", + Rel: "self", + }, + }, + }, + } + + th.AssertDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestNonJSONCannotBeExtractedIntoAPIVersions(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + if _, err := ExtractAPIVersions(page); err == nil { + t.Fatalf("Expected error, got nil") + } + return true, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/results.go new file mode 100644 index 000000000000..0700ab0afb8a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/results.go @@ -0,0 +1,42 @@ +package apiversions + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// APIVersion represents an API version for Neutron. It contains the status of +// the API, and its unique ID. +type APIVersion struct { + Status string `mapstructure:"status"` + ID string `mapstructure:"id"` + Links []gophercloud.Link `mapstructure:"links"` +} + +// APIVersionPage is the page returned by a pager when traversing over a +// collection of API versions. +type APIVersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an APIVersionPage struct is empty. +func (r APIVersionPage) IsEmpty() (bool, error) { + is, err := ExtractAPIVersions(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +// ExtractAPIVersions takes a collection page, extracts all of the elements, +// and returns them a slice of APIVersion structs. It is effectively a cast. +func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) { + var resp struct { + Versions []APIVersion `mapstructure:"versions"` + } + + err := mapstructure.Decode(page.(APIVersionPage).Body, &resp) + + return resp.Versions, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/urls.go new file mode 100644 index 000000000000..55d6e0e7a436 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/apiversions/urls.go @@ -0,0 +1,7 @@ +package apiversions + +import "github.com/rackspace/gophercloud" + +func apiVersionsURL(c *gophercloud.ServiceClient) string { + return c.Endpoint +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/doc.go new file mode 100644 index 000000000000..183e8dfa76d0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/doc.go @@ -0,0 +1,2 @@ +// Package buildinfo provides build information about heat deployments. +package buildinfo diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/fixtures.go new file mode 100644 index 000000000000..20ea09b4425a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/fixtures.go @@ -0,0 +1,45 @@ +package buildinfo + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// GetExpected represents the expected object from a Get request. +var GetExpected = &BuildInfo{ + API: Revision{ + Revision: "2.4.5", + }, + Engine: Revision{ + Revision: "1.2.1", + }, +} + +// GetOutput represents the response body from a Get request. +const GetOutput = ` +{ + "api": { + "revision": "2.4.5" + }, + "engine": { + "revision": "1.2.1" + } +}` + +// HandleGetSuccessfully creates an HTTP handler at `/build_info` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/build_info", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/requests.go new file mode 100644 index 000000000000..9e03e5cc85b5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/requests.go @@ -0,0 +1,10 @@ +package buildinfo + +import "github.com/rackspace/gophercloud" + +// Get retreives data for the given stack template. +func Get(c *gophercloud.ServiceClient) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c), &res.Body, nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/requests_test.go new file mode 100644 index 000000000000..1e0fe230d66d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/requests_test.go @@ -0,0 +1,20 @@ +package buildinfo + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestGetTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := Get(fake.ServiceClient()).Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/results.go new file mode 100644 index 000000000000..683a434a0532 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/results.go @@ -0,0 +1,37 @@ +package buildinfo + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" +) + +// Revision represents the API/Engine revision of a Heat deployment. +type Revision struct { + Revision string `mapstructure:"revision"` +} + +// BuildInfo represents the build information for a Heat deployment. +type BuildInfo struct { + API Revision `mapstructure:"api"` + Engine Revision `mapstructure:"engine"` +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a BuildInfo object and is called after a +// Get operation. +func (r GetResult) Extract() (*BuildInfo, error) { + if r.Err != nil { + return nil, r.Err + } + + var res BuildInfo + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + return &res, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/urls.go new file mode 100644 index 000000000000..2c873d02358e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo/urls.go @@ -0,0 +1,7 @@ +package buildinfo + +import "github.com/rackspace/gophercloud" + +func getURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("build_info") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/doc.go new file mode 100644 index 000000000000..51cdd97473cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/doc.go @@ -0,0 +1,4 @@ +// Package stackevents provides operations for finding, listing, and retrieving +// stack events. Stack events are events that take place on stacks such as +// updating and abandoning. +package stackevents diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/fixtures.go new file mode 100644 index 000000000000..016ae003b32f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/fixtures.go @@ -0,0 +1,446 @@ +package stackevents + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// FindExpected represents the expected object from a Find request. +var FindExpected = []Event{ + Event{ + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + Rel: "self", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_IN_PROGRESS", + PhysicalResourceID: "", + ID: "06feb26f-9298-4a9b-8749-9d770e5d577a", + }, + Event{ + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC), + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + Rel: "self", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_COMPLETE", + PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf", + ID: "93940999-7d40-44ae-8de4-19624e7b8d18", + }, +} + +// FindOutput represents the response body from a Find request. +const FindOutput = ` +{ + "events": [ + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:11Z", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": null, + "id": "06feb26f-9298-4a9b-8749-9d770e5d577a" + }, + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:27Z", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_COMPLETE", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "id": "93940999-7d40-44ae-8de4-19624e7b8d18" + } + ] +}` + +// HandleFindSuccessfully creates an HTTP handler at `/stacks/postman_stack/events` +// on the test handler mux that responds with a `Find` response. +func HandleFindSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/postman_stack/events", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// ListExpected represents the expected object from a List request. +var ListExpected = []Event{ + Event{ + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + Rel: "self", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_IN_PROGRESS", + PhysicalResourceID: "", + ID: "06feb26f-9298-4a9b-8749-9d770e5d577a", + }, + Event{ + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC), + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + Rel: "self", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_COMPLETE", + PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf", + ID: "93940999-7d40-44ae-8de4-19624e7b8d18", + }, +} + +// ListOutput represents the response body from a List request. +const ListOutput = ` +{ + "events": [ + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:11Z", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": null, + "id": "06feb26f-9298-4a9b-8749-9d770e5d577a" + }, + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:27Z", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_COMPLETE", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "id": "93940999-7d40-44ae-8de4-19624e7b8d18" + } + ] +}` + +// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/events` +// on the test handler mux that responds with a `List` response. +func HandleListSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/events", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "93940999-7d40-44ae-8de4-19624e7b8d18": + fmt.Fprintf(w, `{"events":[]}`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// ListResourceEventsExpected represents the expected object from a ListResourceEvents request. +var ListResourceEventsExpected = []Event{ + Event{ + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + Rel: "self", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_IN_PROGRESS", + PhysicalResourceID: "", + ID: "06feb26f-9298-4a9b-8749-9d770e5d577a", + }, + Event{ + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC), + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + Rel: "self", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_COMPLETE", + PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf", + ID: "93940999-7d40-44ae-8de4-19624e7b8d18", + }, +} + +// ListResourceEventsOutput represents the response body from a ListResourceEvents request. +const ListResourceEventsOutput = ` +{ + "events": [ + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:11Z", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": null, + "id": "06feb26f-9298-4a9b-8749-9d770e5d577a" + }, + { + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:27Z", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_COMPLETE", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "id": "93940999-7d40-44ae-8de4-19624e7b8d18" + } + ] +}` + +// HandleListResourceEventsSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events` +// on the test handler mux that responds with a `ListResourceEvents` response. +func HandleListResourceEventsSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "93940999-7d40-44ae-8de4-19624e7b8d18": + fmt.Fprintf(w, `{"events":[]}`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// GetExpected represents the expected object from a Get request. +var GetExpected = &Event{ + ResourceName: "hello_world", + Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC), + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + Rel: "self", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "resource", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalResourceID: "hello_world", + ResourceStatusReason: "state changed", + ResourceStatus: "CREATE_COMPLETE", + PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf", + ID: "93940999-7d40-44ae-8de4-19624e7b8d18", +} + +// GetOutput represents the response body from a Get request. +const GetOutput = ` +{ + "event":{ + "resource_name": "hello_world", + "event_time": "2015-02-05T21:33:27Z", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "resource" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "resource_status": "CREATE_COMPLETE", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "id": "93940999-7d40-44ae-8de4-19624e7b8d18" + } +}` + +// HandleGetSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events/93940999-7d40-44ae-8de4-19624e7b8d18` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events/93940999-7d40-44ae-8de4-19624e7b8d18", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/requests.go new file mode 100644 index 000000000000..53c39160206b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/requests.go @@ -0,0 +1,203 @@ +package stackevents + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Find retrieves stack events for the given stack name. +func Find(c *gophercloud.ServiceClient, stackName string) FindResult { + var res FindResult + + _, res.Err = c.Request("GET", findURL(c, stackName), gophercloud.RequestOpts{ + JSONResponse: &res.Body, + }) + return res +} + +// SortDir is a type for specifying in which direction to sort a list of events. +type SortDir string + +// SortKey is a type for specifying by which key to sort a list of events. +type SortKey string + +// ResourceStatus is a type for specifying by which resource status to filter a +// list of events. +type ResourceStatus string + +// ResourceAction is a type for specifying by which resource action to filter a +// list of events. +type ResourceAction string + +var ( + // ResourceStatusInProgress is used to filter a List request by the 'IN_PROGRESS' status. + ResourceStatusInProgress ResourceStatus = "IN_PROGRESS" + // ResourceStatusComplete is used to filter a List request by the 'COMPLETE' status. + ResourceStatusComplete ResourceStatus = "COMPLETE" + // ResourceStatusFailed is used to filter a List request by the 'FAILED' status. + ResourceStatusFailed ResourceStatus = "FAILED" + + // ResourceActionCreate is used to filter a List request by the 'CREATE' action. + ResourceActionCreate ResourceAction = "CREATE" + // ResourceActionDelete is used to filter a List request by the 'DELETE' action. + ResourceActionDelete ResourceAction = "DELETE" + // ResourceActionUpdate is used to filter a List request by the 'UPDATE' action. + ResourceActionUpdate ResourceAction = "UPDATE" + // ResourceActionRollback is used to filter a List request by the 'ROLLBACK' action. + ResourceActionRollback ResourceAction = "ROLLBACK" + // ResourceActionSuspend is used to filter a List request by the 'SUSPEND' action. + ResourceActionSuspend ResourceAction = "SUSPEND" + // ResourceActionResume is used to filter a List request by the 'RESUME' action. + ResourceActionResume ResourceAction = "RESUME" + // ResourceActionAbandon is used to filter a List request by the 'ABANDON' action. + ResourceActionAbandon ResourceAction = "ABANDON" + + // SortAsc is used to sort a list of stacks in ascending order. + SortAsc SortDir = "asc" + // SortDesc is used to sort a list of stacks in descending order. + SortDesc SortDir = "desc" + + // SortName is used to sort a list of stacks by name. + SortName SortKey = "name" + // SortResourceType is used to sort a list of stacks by resource type. + SortResourceType SortKey = "resource_type" + // SortCreatedAt is used to sort a list of stacks by date created. + SortCreatedAt SortKey = "created_at" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStackEventListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Marker and Limit are used for pagination. +type ListOpts struct { + // The stack resource ID with which to start the listing. + Marker string `q:"marker"` + // Integer value for the limit of values to return. + Limit int `q:"limit"` + // Filters the event list by the specified ResourceAction. You can use this + // filter multiple times to filter by multiple resource actions: CREATE, DELETE, + // UPDATE, ROLLBACK, SUSPEND, RESUME or ADOPT. + ResourceActions []ResourceAction `q:"resource_action"` + // Filters the event list by the specified resource_status. You can use this + // filter multiple times to filter by multiple resource statuses: IN_PROGRESS, + // COMPLETE or FAILED. + ResourceStatuses []ResourceStatus `q:"resource_status"` + // Filters the event list by the specified resource_name. You can use this + // filter multiple times to filter by multiple resource names. + ResourceNames []string `q:"resource_name"` + // Filters the event list by the specified resource_type. You can use this + // filter multiple times to filter by multiple resource types: OS::Nova::Server, + // OS::Cinder::Volume, and so on. + ResourceTypes []string `q:"resource_type"` + // Sorts the event list by: resource_type or created_at. + SortKey SortKey `q:"sort_keys"` + // The sort direction of the event list. Which is asc (ascending) or desc (descending). + SortDir SortDir `q:"sort_dir"` +} + +// ToStackEventListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToStackEventListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List makes a request against the API to list resources for the given stack. +func List(client *gophercloud.ServiceClient, stackName, stackID string, opts ListOptsBuilder) pagination.Pager { + url := listURL(client, stackName, stackID) + + if opts != nil { + query, err := opts.ToStackEventListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPageFn := func(r pagination.PageResult) pagination.Page { + p := EventPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + return pagination.NewPager(client, url, createPageFn) +} + +// ListResourceEventsOptsBuilder allows extensions to add additional parameters to the +// ListResourceEvents request. +type ListResourceEventsOptsBuilder interface { + ToResourceEventListQuery() (string, error) +} + +// ListResourceEventsOpts allows the filtering and sorting of paginated resource events through +// the API. Marker and Limit are used for pagination. +type ListResourceEventsOpts struct { + // The stack resource ID with which to start the listing. + Marker string `q:"marker"` + // Integer value for the limit of values to return. + Limit int `q:"limit"` + // Filters the event list by the specified ResourceAction. You can use this + // filter multiple times to filter by multiple resource actions: CREATE, DELETE, + // UPDATE, ROLLBACK, SUSPEND, RESUME or ADOPT. + ResourceActions []string `q:"resource_action"` + // Filters the event list by the specified resource_status. You can use this + // filter multiple times to filter by multiple resource statuses: IN_PROGRESS, + // COMPLETE or FAILED. + ResourceStatuses []string `q:"resource_status"` + // Filters the event list by the specified resource_name. You can use this + // filter multiple times to filter by multiple resource names. + ResourceNames []string `q:"resource_name"` + // Filters the event list by the specified resource_type. You can use this + // filter multiple times to filter by multiple resource types: OS::Nova::Server, + // OS::Cinder::Volume, and so on. + ResourceTypes []string `q:"resource_type"` + // Sorts the event list by: resource_type or created_at. + SortKey SortKey `q:"sort_keys"` + // The sort direction of the event list. Which is asc (ascending) or desc (descending). + SortDir SortDir `q:"sort_dir"` +} + +// ToResourceEventsListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToResourceEventsListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListResourceEvents makes a request against the API to list resources for the given stack. +func ListResourceEvents(client *gophercloud.ServiceClient, stackName, stackID, resourceName string, opts ListResourceEventsOptsBuilder) pagination.Pager { + url := listResourceEventsURL(client, stackName, stackID, resourceName) + + if opts != nil { + query, err := opts.ToResourceEventListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPageFn := func(r pagination.PageResult) pagination.Page { + p := EventPage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + return pagination.NewPager(client, url, createPageFn) +} + +// Get retreives data for the given stack resource. +func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c, stackName, stackID, resourceName, eventID), &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/requests_test.go new file mode 100644 index 000000000000..a4da4d04e61d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/requests_test.go @@ -0,0 +1,71 @@ +package stackevents + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestFindEvents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleFindSuccessfully(t, FindOutput) + + actual, err := Find(fake.ServiceClient(), "postman_stack").Extract() + th.AssertNoErr(t, err) + + expected := FindExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t, ListOutput) + + count := 0 + err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractEvents(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListResourceEvents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListResourceEventsSuccessfully(t, ListResourceEventsOutput) + + count := 0 + err := ListResourceEvents(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractEvents(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListResourceEventsExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetEvent(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := Get(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", "93940999-7d40-44ae-8de4-19624e7b8d18").Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/results.go new file mode 100644 index 000000000000..3c8f1da49106 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/results.go @@ -0,0 +1,172 @@ +package stackevents + +import ( + "fmt" + "reflect" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Event represents a stack event. +type Event struct { + // The name of the resource for which the event occurred. + ResourceName string `mapstructure:"resource_name"` + // The time the event occurred. + Time time.Time `mapstructure:"-"` + // The URLs to the event. + Links []gophercloud.Link `mapstructure:"links"` + // The logical ID of the stack resource. + LogicalResourceID string `mapstructure:"logical_resource_id"` + // The reason of the status of the event. + ResourceStatusReason string `mapstructure:"resource_status_reason"` + // The status of the event. + ResourceStatus string `mapstructure:"resource_status"` + // The physical ID of the stack resource. + PhysicalResourceID string `mapstructure:"physical_resource_id"` + // The event ID. + ID string `mapstructure:"id"` + // Properties of the stack resource. + ResourceProperties map[string]interface{} `mapstructure:"resource_properties"` +} + +// FindResult represents the result of a Find operation. +type FindResult struct { + gophercloud.Result +} + +// Extract returns a slice of Event objects and is called after a +// Find operation. +func (r FindResult) Extract() ([]Event, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Res []Event `mapstructure:"events"` + } + + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + events := r.Body.(map[string]interface{})["events"].([]interface{}) + + for i, eventRaw := range events { + event := eventRaw.(map[string]interface{}) + if date, ok := event["event_time"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.Res[i].Time = t + } + } + + return res.Res, nil +} + +// EventPage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractResources call. +type EventPage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r EventPage) IsEmpty() (bool, error) { + events, err := ExtractEvents(r) + if err != nil { + return true, err + } + return len(events) == 0, nil +} + +// LastMarker returns the last stack ID in a ListResult. +func (r EventPage) LastMarker() (string, error) { + events, err := ExtractEvents(r) + if err != nil { + return "", err + } + if len(events) == 0 { + return "", nil + } + return events[len(events)-1].ID, nil +} + +// ExtractEvents interprets the results of a single page from a List() call, producing a slice of Event entities. +func ExtractEvents(page pagination.Page) ([]Event, error) { + casted := page.(EventPage).Body + + var res struct { + Res []Event `mapstructure:"events"` + } + + if err := mapstructure.Decode(casted, &res); err != nil { + return nil, err + } + + var events []interface{} + switch casted.(type) { + case map[string]interface{}: + events = casted.(map[string]interface{})["events"].([]interface{}) + case map[string][]interface{}: + events = casted.(map[string][]interface{})["events"] + default: + return res.Res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted)) + } + + for i, eventRaw := range events { + event := eventRaw.(map[string]interface{}) + if date, ok := event["event_time"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.Res[i].Time = t + } + } + + return res.Res, nil +} + +// ExtractResourceEvents interprets the results of a single page from a +// ListResourceEvents() call, producing a slice of Event entities. +func ExtractResourceEvents(page pagination.Page) ([]Event, error) { + return ExtractEvents(page) +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns a pointer to an Event object and is called after a +// Get operation. +func (r GetResult) Extract() (*Event, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Res *Event `mapstructure:"event"` + } + + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + event := r.Body.(map[string]interface{})["event"].(map[string]interface{}) + + if date, ok := event["event_time"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.Res.Time = t + } + + return res.Res, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/urls.go new file mode 100644 index 000000000000..8b5eceb1704c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents/urls.go @@ -0,0 +1,19 @@ +package stackevents + +import "github.com/rackspace/gophercloud" + +func findURL(c *gophercloud.ServiceClient, stackName string) string { + return c.ServiceURL("stacks", stackName, "events") +} + +func listURL(c *gophercloud.ServiceClient, stackName, stackID string) string { + return c.ServiceURL("stacks", stackName, stackID, "events") +} + +func listResourceEventsURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string { + return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "events") +} + +func getURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) string { + return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "events", eventID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/doc.go new file mode 100644 index 000000000000..e4f8b08dcc7e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/doc.go @@ -0,0 +1,5 @@ +// Package stackresources provides operations for working with stack resources. +// A resource is a template artifact that represents some component of your +// desired architecture (a Cloud Server, a group of scaled Cloud Servers, a load +// balancer, some configuration management system, and so forth). +package stackresources diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/fixtures.go new file mode 100644 index 000000000000..0b930f4841a2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/fixtures.go @@ -0,0 +1,451 @@ +package stackresources + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// FindExpected represents the expected object from a Find request. +var FindExpected = []Resource{ + Resource{ + Name: "hello_world", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "self", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalID: "hello_world", + StatusReason: "state changed", + UpdatedTime: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + RequiredBy: []interface{}{}, + Status: "CREATE_IN_PROGRESS", + PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf", + Type: "OS::Nova::Server", + }, +} + +// FindOutput represents the response body from a Find request. +const FindOutput = ` +{ + "resources": [ + { + "resource_name": "hello_world", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "updated_time": "2015-02-05T21:33:11Z", + "required_by": [], + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "resource_type": "OS::Nova::Server" + } + ] +}` + +// HandleFindSuccessfully creates an HTTP handler at `/stacks/hello_world/resources` +// on the test handler mux that responds with a `Find` response. +func HandleFindSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/resources", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// ListExpected represents the expected object from a List request. +var ListExpected = []Resource{ + Resource{ + Name: "hello_world", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + Rel: "self", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + Rel: "stack", + }, + }, + LogicalID: "hello_world", + StatusReason: "state changed", + UpdatedTime: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC), + RequiredBy: []interface{}{}, + Status: "CREATE_IN_PROGRESS", + PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf", + Type: "OS::Nova::Server", + }, +} + +// ListOutput represents the response body from a List request. +const ListOutput = `{ + "resources": [ + { + "resource_name": "hello_world", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b", + "rel": "stack" + } + ], + "logical_resource_id": "hello_world", + "resource_status_reason": "state changed", + "updated_time": "2015-02-05T21:33:11Z", + "required_by": [], + "resource_status": "CREATE_IN_PROGRESS", + "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf", + "resource_type": "OS::Nova::Server" + } +] +}` + +// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources` +// on the test handler mux that responds with a `List` response. +func HandleListSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "49181cd6-169a-4130-9455-31185bbfc5bf": + fmt.Fprintf(w, `{"resources":[]}`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// GetExpected represents the expected object from a Get request. +var GetExpected = &Resource{ + Name: "wordpress_instance", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance", + Rel: "self", + }, + gophercloud.Link{ + Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e", + Rel: "stack", + }, + }, + LogicalID: "wordpress_instance", + StatusReason: "state changed", + UpdatedTime: time.Date(2014, 12, 10, 18, 34, 35, 0, time.UTC), + RequiredBy: []interface{}{}, + Status: "CREATE_COMPLETE", + PhysicalID: "00e3a2fe-c65d-403c-9483-4db9930dd194", + Type: "OS::Nova::Server", +} + +// GetOutput represents the response body from a Get request. +const GetOutput = ` +{ + "resource": { + "resource_name": "wordpress_instance", + "description": "", + "links": [ + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance", + "rel": "self" + }, + { + "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e", + "rel": "stack" + } + ], + "logical_resource_id": "wordpress_instance", + "resource_status": "CREATE_COMPLETE", + "updated_time": "2014-12-10T18:34:35Z", + "required_by": [], + "resource_status_reason": "state changed", + "physical_resource_id": "00e3a2fe-c65d-403c-9483-4db9930dd194", + "resource_type": "OS::Nova::Server" + } +}` + +// HandleGetSuccessfully creates an HTTP handler at `/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// MetadataExpected represents the expected object from a Metadata request. +var MetadataExpected = map[string]string{ + "number": "7", + "animal": "auk", +} + +// MetadataOutput represents the response body from a Metadata request. +const MetadataOutput = ` +{ + "metadata": { + "number": "7", + "animal": "auk" + } +}` + +// HandleMetadataSuccessfully creates an HTTP handler at `/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance/metadata` +// on the test handler mux that responds with a `Metadata` response. +func HandleMetadataSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// ListTypesExpected represents the expected object from a ListTypes request. +var ListTypesExpected = []string{ + "OS::Nova::Server", + "OS::Heat::RandomString", + "OS::Swift::Container", + "OS::Trove::Instance", + "OS::Nova::FloatingIPAssociation", + "OS::Cinder::VolumeAttachment", + "OS::Nova::FloatingIP", + "OS::Nova::KeyPair", +} + +// ListTypesOutput represents the response body from a ListTypes request. +const ListTypesOutput = ` +{ + "resource_types": [ + "OS::Nova::Server", + "OS::Heat::RandomString", + "OS::Swift::Container", + "OS::Trove::Instance", + "OS::Nova::FloatingIPAssociation", + "OS::Cinder::VolumeAttachment", + "OS::Nova::FloatingIP", + "OS::Nova::KeyPair" + ] +}` + +// HandleListTypesSuccessfully creates an HTTP handler at `/resource_types` +// on the test handler mux that responds with a `ListTypes` response. +func HandleListTypesSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/resource_types", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// GetSchemaExpected represents the expected object from a Schema request. +var GetSchemaExpected = &TypeSchema{ + Attributes: map[string]interface{}{ + "an_attribute": map[string]interface{}{ + "description": "An attribute description .", + }, + }, + Properties: map[string]interface{}{ + "a_property": map[string]interface{}{ + "update_allowed": false, + "required": true, + "type": "string", + "description": "A resource description.", + }, + }, + ResourceType: "OS::Heat::AResourceName", +} + +// GetSchemaOutput represents the response body from a Schema request. +const GetSchemaOutput = ` +{ + "attributes": { + "an_attribute": { + "description": "An attribute description ." + } + }, + "properties": { + "a_property": { + "update_allowed": false, + "required": true, + "type": "string", + "description": "A resource description." + } + }, + "resource_type": "OS::Heat::AResourceName" +}` + +// HandleGetSchemaSuccessfully creates an HTTP handler at `/resource_types/OS::Heat::AResourceName` +// on the test handler mux that responds with a `Schema` response. +func HandleGetSchemaSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/resource_types/OS::Heat::AResourceName", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// GetTemplateExpected represents the expected object from a Template request. +var GetTemplateExpected = &TypeTemplate{ + HeatTemplateFormatVersion: "2012-12-12", + Outputs: map[string]interface{}{ + "private_key": map[string]interface{}{ + "Description": "The private key if it has been saved.", + "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"private_key\"]}", + }, + "public_key": map[string]interface{}{ + "Description": "The public key.", + "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"public_key\"]}", + }, + }, + Parameters: map[string]interface{}{ + "name": map[string]interface{}{ + "Description": "The name of the key pair.", + "Type": "String", + }, + "public_key": map[string]interface{}{ + "Description": "The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.", + "Type": "String", + }, + "save_private_key": map[string]interface{}{ + "AllowedValues": []string{ + "True", + "true", + "False", + "false", + }, + "Default": false, + "Description": "True if the system should remember a generated private key; False otherwise.", + "Type": "String", + }, + }, + Resources: map[string]interface{}{ + "KeyPair": map[string]interface{}{ + "Properties": map[string]interface{}{ + "name": map[string]interface{}{ + "Ref": "name", + }, + "public_key": map[string]interface{}{ + "Ref": "public_key", + }, + "save_private_key": map[string]interface{}{ + "Ref": "save_private_key", + }, + }, + "Type": "OS::Nova::KeyPair", + }, + }, +} + +// GetTemplateOutput represents the response body from a Template request. +const GetTemplateOutput = ` +{ + "HeatTemplateFormatVersion": "2012-12-12", + "Outputs": { + "private_key": { + "Description": "The private key if it has been saved.", + "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"private_key\"]}" + }, + "public_key": { + "Description": "The public key.", + "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"public_key\"]}" + } + }, + "Parameters": { + "name": { + "Description": "The name of the key pair.", + "Type": "String" + }, + "public_key": { + "Description": "The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.", + "Type": "String" + }, + "save_private_key": { + "AllowedValues": [ + "True", + "true", + "False", + "false" + ], + "Default": false, + "Description": "True if the system should remember a generated private key; False otherwise.", + "Type": "String" + } + }, + "Resources": { + "KeyPair": { + "Properties": { + "name": { + "Ref": "name" + }, + "public_key": { + "Ref": "public_key" + }, + "save_private_key": { + "Ref": "save_private_key" + } + }, + "Type": "OS::Nova::KeyPair" + } + } +}` + +// HandleGetTemplateSuccessfully creates an HTTP handler at `/resource_types/OS::Heat::AResourceName/template` +// on the test handler mux that responds with a `Template` response. +func HandleGetTemplateSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/resource_types/OS::Heat::AResourceName/template", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/requests.go new file mode 100644 index 000000000000..ee9c3c250cc1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/requests.go @@ -0,0 +1,121 @@ +package stackresources + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Find retrieves stack resources for the given stack name. +func Find(c *gophercloud.ServiceClient, stackName string) FindResult { + var res FindResult + + // Send request to API + _, res.Err = c.Request("GET", findURL(c, stackName), gophercloud.RequestOpts{ + JSONResponse: &res.Body, + }) + return res +} + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStackResourceListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Marker and Limit are used for pagination. +type ListOpts struct { + // The stack resource ID with which to start the listing. + Marker string `q:"marker"` + + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // Include resources from nest stacks up to Depth levels of recursion. + Depth int `q:"nested_depth"` +} + +// ToStackResourceListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToStackResourceListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List makes a request against the API to list resources for the given stack. +func List(client *gophercloud.ServiceClient, stackName, stackID string, opts ListOptsBuilder) pagination.Pager { + url := listURL(client, stackName, stackID) + + if opts != nil { + query, err := opts.ToStackResourceListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPageFn := func(r pagination.PageResult) pagination.Page { + p := ResourcePage{pagination.MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + return pagination.NewPager(client, url, createPageFn) +} + +// Get retreives data for the given stack resource. +func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) GetResult { + var res GetResult + + // Send request to API + _, res.Err = c.Get(getURL(c, stackName, stackID, resourceName), &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Metadata retreives the metadata for the given stack resource. +func Metadata(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) MetadataResult { + var res MetadataResult + + // Send request to API + _, res.Err = c.Get(metadataURL(c, stackName, stackID, resourceName), &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// ListTypes makes a request against the API to list resource types. +func ListTypes(client *gophercloud.ServiceClient) pagination.Pager { + url := listTypesURL(client) + + createPageFn := func(r pagination.PageResult) pagination.Page { + return ResourceTypePage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(client, url, createPageFn) +} + +// Schema retreives the schema for the given resource type. +func Schema(c *gophercloud.ServiceClient, resourceType string) SchemaResult { + var res SchemaResult + + // Send request to API + _, res.Err = c.Get(schemaURL(c, resourceType), &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Template retreives the template representation for the given resource type. +func Template(c *gophercloud.ServiceClient, resourceType string) TemplateResult { + var res TemplateResult + + // Send request to API + _, res.Err = c.Get(templateURL(c, resourceType), &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/requests_test.go new file mode 100644 index 000000000000..f1378785f890 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/requests_test.go @@ -0,0 +1,107 @@ +package stackresources + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestFindResources(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleFindSuccessfully(t, FindOutput) + + actual, err := Find(fake.ServiceClient(), "hello_world").Extract() + th.AssertNoErr(t, err) + + expected := FindExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestListResources(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t, ListOutput) + + count := 0 + err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractResources(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetResource(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := Get(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestResourceMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleMetadataSuccessfully(t, MetadataOutput) + + actual, err := Metadata(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract() + th.AssertNoErr(t, err) + + expected := MetadataExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestListResourceTypes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListTypesSuccessfully(t, ListTypesOutput) + + count := 0 + err := ListTypes(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractResourceTypes(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListTypesExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetResourceSchema(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSchemaSuccessfully(t, GetSchemaOutput) + + actual, err := Schema(fake.ServiceClient(), "OS::Heat::AResourceName").Extract() + th.AssertNoErr(t, err) + + expected := GetSchemaExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestGetResourceTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetTemplateSuccessfully(t, GetTemplateOutput) + + actual, err := Template(fake.ServiceClient(), "OS::Heat::AResourceName").Extract() + th.AssertNoErr(t, err) + + expected := GetTemplateExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/results.go new file mode 100644 index 000000000000..69f21daef39a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/results.go @@ -0,0 +1,260 @@ +package stackresources + +import ( + "fmt" + "reflect" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Resource represents a stack resource. +type Resource struct { + Links []gophercloud.Link `mapstructure:"links"` + LogicalID string `mapstructure:"logical_resource_id"` + Name string `mapstructure:"resource_name"` + PhysicalID string `mapstructure:"physical_resource_id"` + RequiredBy []interface{} `mapstructure:"required_by"` + Status string `mapstructure:"resource_status"` + StatusReason string `mapstructure:"resource_status_reason"` + Type string `mapstructure:"resource_type"` + UpdatedTime time.Time `mapstructure:"-"` +} + +// FindResult represents the result of a Find operation. +type FindResult struct { + gophercloud.Result +} + +// Extract returns a slice of Resource objects and is called after a +// Find operation. +func (r FindResult) Extract() ([]Resource, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Res []Resource `mapstructure:"resources"` + } + + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + resources := r.Body.(map[string]interface{})["resources"].([]interface{}) + + for i, resourceRaw := range resources { + resource := resourceRaw.(map[string]interface{}) + if date, ok := resource["updated_time"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.Res[i].UpdatedTime = t + } + } + + return res.Res, nil +} + +// ResourcePage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractResources call. +type ResourcePage struct { + pagination.MarkerPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r ResourcePage) IsEmpty() (bool, error) { + resources, err := ExtractResources(r) + if err != nil { + return true, err + } + return len(resources) == 0, nil +} + +// LastMarker returns the last container name in a ListResult. +func (r ResourcePage) LastMarker() (string, error) { + resources, err := ExtractResources(r) + if err != nil { + return "", err + } + if len(resources) == 0 { + return "", nil + } + return resources[len(resources)-1].PhysicalID, nil +} + +// ExtractResources interprets the results of a single page from a List() call, producing a slice of Resource entities. +func ExtractResources(page pagination.Page) ([]Resource, error) { + casted := page.(ResourcePage).Body + + var response struct { + Resources []Resource `mapstructure:"resources"` + } + err := mapstructure.Decode(casted, &response) + + var resources []interface{} + switch casted.(type) { + case map[string]interface{}: + resources = casted.(map[string]interface{})["resources"].([]interface{}) + case map[string][]interface{}: + resources = casted.(map[string][]interface{})["resources"] + default: + return response.Resources, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted)) + } + + for i, resourceRaw := range resources { + resource := resourceRaw.(map[string]interface{}) + if date, ok := resource["updated_time"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + response.Resources[i].UpdatedTime = t + } + } + + return response.Resources, err +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a Resource object and is called after a +// Get operation. +func (r GetResult) Extract() (*Resource, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Res *Resource `mapstructure:"resource"` + } + + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + resource := r.Body.(map[string]interface{})["resource"].(map[string]interface{}) + + if date, ok := resource["updated_time"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.Res.UpdatedTime = t + } + + return res.Res, nil +} + +// MetadataResult represents the result of a Metadata operation. +type MetadataResult struct { + gophercloud.Result +} + +// Extract returns a map object and is called after a +// Metadata operation. +func (r MetadataResult) Extract() (map[string]string, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Meta map[string]string `mapstructure:"metadata"` + } + + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + return res.Meta, nil +} + +// ResourceTypePage abstracts the raw results of making a ListTypes() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractResourceTypes call. +type ResourceTypePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ResourceTypePage contains no resource types. +func (r ResourceTypePage) IsEmpty() (bool, error) { + rts, err := ExtractResourceTypes(r) + if err != nil { + return true, err + } + return len(rts) == 0, nil +} + +// ExtractResourceTypes extracts and returns resource types. +func ExtractResourceTypes(page pagination.Page) ([]string, error) { + var response struct { + ResourceTypes []string `mapstructure:"resource_types"` + } + + err := mapstructure.Decode(page.(ResourceTypePage).Body, &response) + return response.ResourceTypes, err +} + +// TypeSchema represents a stack resource schema. +type TypeSchema struct { + Attributes map[string]interface{} `mapstructure:"attributes"` + Properties map[string]interface{} `mapstrucutre:"properties"` + ResourceType string `mapstructure:"resource_type"` +} + +// SchemaResult represents the result of a Schema operation. +type SchemaResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a TypeSchema object and is called after a +// Schema operation. +func (r SchemaResult) Extract() (*TypeSchema, error) { + if r.Err != nil { + return nil, r.Err + } + + var res TypeSchema + + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// TypeTemplate represents a stack resource template. +type TypeTemplate struct { + HeatTemplateFormatVersion string + Outputs map[string]interface{} + Parameters map[string]interface{} + Resources map[string]interface{} +} + +// TemplateResult represents the result of a Template operation. +type TemplateResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a TypeTemplate object and is called after a +// Template operation. +func (r TemplateResult) Extract() (*TypeTemplate, error) { + if r.Err != nil { + return nil, r.Err + } + + var res TypeTemplate + + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + return &res, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/urls.go new file mode 100644 index 000000000000..ef078d9c9b4d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources/urls.go @@ -0,0 +1,31 @@ +package stackresources + +import "github.com/rackspace/gophercloud" + +func findURL(c *gophercloud.ServiceClient, stackName string) string { + return c.ServiceURL("stacks", stackName, "resources") +} + +func listURL(c *gophercloud.ServiceClient, stackName, stackID string) string { + return c.ServiceURL("stacks", stackName, stackID, "resources") +} + +func getURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string { + return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName) +} + +func metadataURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string { + return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "metadata") +} + +func listTypesURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("resource_types") +} + +func schemaURL(c *gophercloud.ServiceClient, typeName string) string { + return c.ServiceURL("resource_types", typeName) +} + +func templateURL(c *gophercloud.ServiceClient, typeName string) string { + return c.ServiceURL("resource_types", typeName, "template") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/doc.go new file mode 100644 index 000000000000..19231b5137e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/doc.go @@ -0,0 +1,8 @@ +// Package stacks provides operation for working with Heat stacks. A stack is a +// group of resources (servers, load balancers, databases, and so forth) +// combined to fulfill a useful purpose. Based on a template, Heat orchestration +// engine creates an instantiated set of resources (a stack) to run the +// application framework or component specified (in the template). A stack is a +// running instance of a template. The result of creating a stack is a deployment +// of the application framework or component. +package stacks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/fixtures.go new file mode 100644 index 000000000000..6d3e9597a21b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/fixtures.go @@ -0,0 +1,374 @@ +package stacks + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// CreateExpected represents the expected object from a Create request. +var CreateExpected = &CreatedStack{ + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://168.28.170.117:8004/v1/98606384f58drad0bhdb7d02779549ac/stacks/stackcreated/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, +} + +// CreateOutput represents the response body from a Create request. +const CreateOutput = ` +{ + "stack": { + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "links": [ + { + "href": "http://168.28.170.117:8004/v1/98606384f58drad0bhdb7d02779549ac/stacks/stackcreated/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "rel": "self" + } + ] + } +}` + +// HandleCreateSuccessfully creates an HTTP handler at `/stacks` on the test handler mux +// that responds with a `Create` response. +func HandleCreateSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, output) + }) +} + +// ListExpected represents the expected object from a List request. +var ListExpected = []ListedStack{ + ListedStack{ + Description: "Simple template to test heat commands", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, + StatusReason: "Stack CREATE completed successfully", + Name: "postman_stack", + CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC), + Status: "CREATE_COMPLETE", + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + }, + ListedStack{ + Description: "Simple template to test heat commands", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", + Rel: "self", + }, + }, + StatusReason: "Stack successfully updated", + Name: "gophercloud-test-stack-2", + CreationTime: time.Date(2014, 12, 11, 17, 39, 16, 0, time.UTC), + UpdatedTime: time.Date(2014, 12, 11, 17, 40, 37, 0, time.UTC), + Status: "UPDATE_COMPLETE", + ID: "db6977b2-27aa-4775-9ae7-6213212d4ada", + }, +} + +// FullListOutput represents the response body from a List request without a marker. +const FullListOutput = ` +{ + "stacks": [ + { + "description": "Simple template to test heat commands", + "links": [ + { + "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "rel": "self" + } + ], + "stack_status_reason": "Stack CREATE completed successfully", + "stack_name": "postman_stack", + "creation_time": "2015-02-03T20:07:39Z", + "updated_time": null, + "stack_status": "CREATE_COMPLETE", + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87" + }, + { + "description": "Simple template to test heat commands", + "links": [ + { + "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", + "rel": "self" + } + ], + "stack_status_reason": "Stack successfully updated", + "stack_name": "gophercloud-test-stack-2", + "creation_time": "2014-12-11T17:39:16Z", + "updated_time": "2014-12-11T17:40:37Z", + "stack_status": "UPDATE_COMPLETE", + "id": "db6977b2-27aa-4775-9ae7-6213212d4ada" + } + ] +} +` + +// HandleListSuccessfully creates an HTTP handler at `/stacks` on the test handler mux +// that responds with a `List` response. +func HandleListSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, output) + case "db6977b2-27aa-4775-9ae7-6213212d4ada": + fmt.Fprintf(w, `[]`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + +// GetExpected represents the expected object from a Get request. +var GetExpected = &RetrievedStack{ + DisableRollback: true, + Description: "Simple template to test heat commands", + Parameters: map[string]string{ + "flavor": "m1.tiny", + "OS::stack_name": "postman_stack", + "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + }, + StatusReason: "Stack CREATE completed successfully", + Name: "postman_stack", + Outputs: []map[string]interface{}{}, + CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC), + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, + Capabilities: []interface{}{}, + NotificationTopics: []interface{}{}, + Status: "CREATE_COMPLETE", + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + TemplateDescription: "Simple template to test heat commands", +} + +// GetOutput represents the response body from a Get request. +const GetOutput = ` +{ + "stack": { + "disable_rollback": true, + "description": "Simple template to test heat commands", + "parameters": { + "flavor": "m1.tiny", + "OS::stack_name": "postman_stack", + "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87" + }, + "stack_status_reason": "Stack CREATE completed successfully", + "stack_name": "postman_stack", + "outputs": [], + "creation_time": "2015-02-03T20:07:39Z", + "links": [ + { + "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "rel": "self" + } + ], + "capabilities": [], + "notification_topics": [], + "timeout_mins": null, + "stack_status": "CREATE_COMPLETE", + "updated_time": null, + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "template_description": "Simple template to test heat commands" + } +} +` + +// HandleGetSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// HandleUpdateSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87` +// on the test handler mux that responds with an `Update` response. +func HandleUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", 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") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) +} + +// HandleDeleteSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87` +// on the test handler mux that responds with a `Delete` response. +func HandleDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) +} + +// GetExpected represents the expected object from a Get request. +var PreviewExpected = &PreviewedStack{ + DisableRollback: true, + Description: "Simple template to test heat commands", + Parameters: map[string]string{ + "flavor": "m1.tiny", + "OS::stack_name": "postman_stack", + "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + }, + StatusReason: "Stack CREATE completed successfully", + Name: "postman_stack", + CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC), + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Rel: "self", + }, + }, + Capabilities: []interface{}{}, + NotificationTopics: []interface{}{}, + Status: "CREATE_COMPLETE", + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + TemplateDescription: "Simple template to test heat commands", +} + +// HandlePreviewSuccessfully creates an HTTP handler at `/stacks/preview` +// on the test handler mux that responds with a `Preview` response. +func HandlePreviewSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/preview", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// AbandonExpected represents the expected object from an Abandon request. +var AbandonExpected = &AbandonedStack{ + Status: "COMPLETE", + Name: "postman_stack", + Template: map[string]interface{}{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": map[string]interface{}{ + "flavor": map[string]interface{}{ + "default": "m1.tiny", + "type": "string", + }, + }, + "resources": map[string]interface{}{ + "hello_world": map[string]interface{}{ + "type": "OS::Nova::Server", + "properties": map[string]interface{}{ + "key_name": "heat_key", + "flavor": map[string]interface{}{ + "get_param": "flavor", + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", + }, + }, + }, + }, + Action: "CREATE", + ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + Resources: map[string]interface{}{ + "hello_world": map[string]interface{}{ + "status": "COMPLETE", + "name": "hello_world", + "resource_id": "8a310d36-46fc-436f-8be4-37a696b8ac63", + "action": "CREATE", + "type": "OS::Nova::Server", + }, + }, +} + +// AbandonOutput represents the response body from an Abandon request. +const AbandonOutput = ` +{ + "status": "COMPLETE", + "name": "postman_stack", + "template": { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type": "OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + }, + "action": "CREATE", + "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87", + "resources": { + "hello_world": { + "status": "COMPLETE", + "name": "hello_world", + "resource_id": "8a310d36-46fc-436f-8be4-37a696b8ac63", + "action": "CREATE", + "type": "OS::Nova::Server", + } + } +}` + +// HandleAbandonSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/abandon` +// on the test handler mux that responds with an `Abandon` response. +func HandleAbandonSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/abandon", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, AbandonOutput) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/requests.go new file mode 100644 index 000000000000..0dd6af2cfa87 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/requests.go @@ -0,0 +1,493 @@ +package stacks + +import ( + "errors" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Rollback is used to specify whether or not a stack can be rolled back. +type Rollback *bool + +var ( + disable = true + // Disable is used to specify that a stack cannot be rolled back. + Disable Rollback = &disable + enable = false + // Enable is used to specify that a stack can be rolled back. + Enable Rollback = &enable +) + +// 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 { + ToStackCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // (REQUIRED) The name of the stack. It must start with an alphabetic character. + Name string + // (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate. + // This value is ignored if Template is supplied inline. + TemplateURL string + // (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value + // is a stringified version of the JSON/YAML template. Since the template will likely + // be located in a file, one way to set this variable is by using ioutil.ReadFile: + // import "io/ioutil" + // var opts stacks.CreateOpts + // b, err := ioutil.ReadFile("path/to/you/template/file.json") + // if err != nil { + // // handle error... + // } + // opts.Template = string(b) + Template string + // (OPTIONAL) Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback Rollback + // (OPTIONAL) A stringified JSON environment for the stack. + Environment string + // (OPTIONAL) A map that maps file names to file contents. It can also be used + // to pass provider template contents. Example: + // Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}` + Files map[string]interface{} + // (OPTIONAL) User-defined parameters to pass to the template. + Parameters map[string]string + // (OPTIONAL) The timeout for stack creation in minutes. + Timeout int +} + +// ToStackCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToStackCreateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.Name == "" { + return s, errors.New("Required field 'Name' not provided.") + } + s["stack_name"] = opts.Name + + if opts.Template != "" { + s["template"] = opts.Template + } else if opts.TemplateURL != "" { + s["template_url"] = opts.TemplateURL + } else { + return s, errors.New("Either Template or TemplateURL must be provided.") + } + + if opts.DisableRollback != nil { + s["disable_rollback"] = &opts.DisableRollback + } + + if opts.Environment != "" { + s["environment"] = opts.Environment + } + if opts.Files != nil { + s["files"] = opts.Files + } + if opts.Parameters != nil { + s["parameters"] = opts.Parameters + } + + if opts.Timeout != 0 { + s["timeout_mins"] = opts.Timeout + } + + return s, nil +} + +// Create accepts a CreateOpts struct and creates a new stack using the values +// provided. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToStackCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Post(createURL(c), reqBody, &res.Body, nil) + return res +} + +// AdoptOptsBuilder is the interface options structs have to satisfy in order +// to be used in the Adopt function 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 AdoptOptsBuilder interface { + ToStackAdoptMap() (map[string]interface{}, error) +} + +// AdoptOpts is the common options struct used in this package's Adopt +// operation. +type AdoptOpts struct { + // (REQUIRED) Existing resources data represented as a string to add to the + // new stack. Data returned by Abandon could be provided as AdoptsStackData. + AdoptStackData string + // (REQUIRED) The name of the stack. It must start with an alphabetic character. + Name string + // (REQUIRED) The timeout for stack creation in minutes. + Timeout int + // (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate. + // This value is ignored if Template is supplied inline. + TemplateURL string + // (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value + // is a stringified version of the JSON/YAML template. Since the template will likely + // be located in a file, one way to set this variable is by using ioutil.ReadFile: + // import "io/ioutil" + // var opts stacks.CreateOpts + // b, err := ioutil.ReadFile("path/to/you/template/file.json") + // if err != nil { + // // handle error... + // } + // opts.Template = string(b) + Template string + // (OPTIONAL) Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback Rollback + // (OPTIONAL) A stringified JSON environment for the stack. + Environment string + // (OPTIONAL) A map that maps file names to file contents. It can also be used + // to pass provider template contents. Example: + // Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}` + Files map[string]interface{} + // (OPTIONAL) User-defined parameters to pass to the template. + Parameters map[string]string +} + +// ToStackAdoptMap casts a CreateOpts struct to a map. +func (opts AdoptOpts) ToStackAdoptMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.Name == "" { + return s, errors.New("Required field 'Name' not provided.") + } + s["stack_name"] = opts.Name + + if opts.Template != "" { + s["template"] = opts.Template + } else if opts.TemplateURL != "" { + s["template_url"] = opts.TemplateURL + } else { + return s, errors.New("Either Template or TemplateURL must be provided.") + } + + if opts.AdoptStackData == "" { + return s, errors.New("Required field 'AdoptStackData' not provided.") + } + s["adopt_stack_data"] = opts.AdoptStackData + + if opts.DisableRollback != nil { + s["disable_rollback"] = &opts.DisableRollback + } + + if opts.Environment != "" { + s["environment"] = opts.Environment + } + if opts.Files != nil { + s["files"] = opts.Files + } + if opts.Parameters != nil { + s["parameters"] = opts.Parameters + } + + if opts.Timeout == 0 { + return nil, errors.New("Required field 'Timeout' not provided.") + } + s["timeout_mins"] = opts.Timeout + + return map[string]interface{}{"stack": s}, nil +} + +// Adopt accepts an AdoptOpts struct and creates a new stack using the resources +// from another stack. +func Adopt(c *gophercloud.ServiceClient, opts AdoptOptsBuilder) AdoptResult { + var res AdoptResult + + reqBody, err := opts.ToStackAdoptMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Post(adoptURL(c), reqBody, &res.Body, nil) + return res +} + +// SortDir is a type for specifying in which direction to sort a list of stacks. +type SortDir string + +// SortKey is a type for specifying by which key to sort a list of stacks. +type SortKey string + +var ( + // SortAsc is used to sort a list of stacks in ascending order. + SortAsc SortDir = "asc" + // SortDesc is used to sort a list of stacks in descending order. + SortDesc SortDir = "desc" + // SortName is used to sort a list of stacks by name. + SortName SortKey = "name" + // SortStatus is used to sort a list of stacks by status. + SortStatus SortKey = "status" + // SortCreatedAt is used to sort a list of stacks by date created. + SortCreatedAt SortKey = "created_at" + // SortUpdatedAt is used to sort a list of stacks by date updated. + SortUpdatedAt SortKey = "updated_at" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToStackListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the network attributes you want to see returned. SortKey allows you to sort +// by a particular network attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey SortKey `q:"sort_keys"` + SortDir SortDir `q:"sort_dir"` +} + +// ToStackListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToStackListQuery() (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 +// stacks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToStackListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + createPage := func(r pagination.PageResult) pagination.Page { + return StackPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} + +// Get retreives a stack based on the stack name and stack ID. +func Get(c *gophercloud.ServiceClient, stackName, stackID string) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c, stackName, stackID), &res.Body, nil) + return res +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the Update operation in this package. +type UpdateOptsBuilder interface { + ToStackUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate. + // This value is ignored if Template is supplied inline. + TemplateURL string + // (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value + // is a stringified version of the JSON/YAML template. Since the template will likely + // be located in a file, one way to set this variable is by using ioutil.ReadFile: + // import "io/ioutil" + // var opts stacks.CreateOpts + // b, err := ioutil.ReadFile("path/to/you/template/file.json") + // if err != nil { + // // handle error... + // } + // opts.Template = string(b) + Template string + // (OPTIONAL) A stringified JSON environment for the stack. + Environment string + // (OPTIONAL) A map that maps file names to file contents. It can also be used + // to pass provider template contents. Example: + // Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}` + Files map[string]interface{} + // (OPTIONAL) User-defined parameters to pass to the template. + Parameters map[string]string + // (OPTIONAL) The timeout for stack creation in minutes. + Timeout int +} + +// ToStackUpdateMap casts a CreateOpts struct to a map. +func (opts UpdateOpts) ToStackUpdateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.Template != "" { + s["template"] = opts.Template + } else if opts.TemplateURL != "" { + s["template_url"] = opts.TemplateURL + } else { + return s, errors.New("Either Template or TemplateURL must be provided.") + } + + if opts.Environment != "" { + s["environment"] = opts.Environment + } + + if opts.Files != nil { + s["files"] = opts.Files + } + + if opts.Parameters != nil { + s["parameters"] = opts.Parameters + } + + if opts.Timeout != 0 { + s["timeout_mins"] = opts.Timeout + } + + return s, nil +} + +// Update accepts an UpdateOpts struct and updates an existing stack using the values +// provided. +func Update(c *gophercloud.ServiceClient, stackName, stackID string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToStackUpdateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Put(updateURL(c, stackName, stackID), reqBody, nil, nil) + return res +} + +// Delete deletes a stack based on the stack name and stack ID. +func Delete(c *gophercloud.ServiceClient, stackName, stackID string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(deleteURL(c, stackName, stackID), nil) + return res +} + +// PreviewOptsBuilder is the interface options structs have to satisfy in order +// to be used in the Preview operation in this package. +type PreviewOptsBuilder interface { + ToStackPreviewMap() (map[string]interface{}, error) +} + +// PreviewOpts contains the common options struct used in this package's Preview +// operation. +type PreviewOpts struct { + // (REQUIRED) The name of the stack. It must start with an alphabetic character. + Name string + // (REQUIRED) The timeout for stack creation in minutes. + Timeout int + // (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate. + // This value is ignored if Template is supplied inline. + TemplateURL string + // (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value + // is a stringified version of the JSON/YAML template. Since the template will likely + // be located in a file, one way to set this variable is by using ioutil.ReadFile: + // import "io/ioutil" + // var opts stacks.CreateOpts + // b, err := ioutil.ReadFile("path/to/you/template/file.json") + // if err != nil { + // // handle error... + // } + // opts.Template = string(b) + Template string + // (OPTIONAL) Enables or disables deletion of all stack resources when a stack + // creation fails. Default is true, meaning all resources are not deleted when + // stack creation fails. + DisableRollback Rollback + // (OPTIONAL) A stringified JSON environment for the stack. + Environment string + // (OPTIONAL) A map that maps file names to file contents. It can also be used + // to pass provider template contents. Example: + // Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}` + Files map[string]interface{} + // (OPTIONAL) User-defined parameters to pass to the template. + Parameters map[string]string +} + +// ToStackPreviewMap casts a PreviewOpts struct to a map. +func (opts PreviewOpts) ToStackPreviewMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.Name == "" { + return s, errors.New("Required field 'Name' not provided.") + } + s["stack_name"] = opts.Name + + if opts.Template != "" { + s["template"] = opts.Template + } else if opts.TemplateURL != "" { + s["template_url"] = opts.TemplateURL + } else { + return s, errors.New("Either Template or TemplateURL must be provided.") + } + + if opts.DisableRollback != nil { + s["disable_rollback"] = &opts.DisableRollback + } + + if opts.Environment != "" { + s["environment"] = opts.Environment + } + if opts.Files != nil { + s["files"] = opts.Files + } + if opts.Parameters != nil { + s["parameters"] = opts.Parameters + } + + if opts.Timeout != 0 { + s["timeout_mins"] = opts.Timeout + } + + return s, nil +} + +// Preview accepts a PreviewOptsBuilder interface and creates a preview of a stack using the values +// provided. +func Preview(c *gophercloud.ServiceClient, opts PreviewOptsBuilder) PreviewResult { + var res PreviewResult + + reqBody, err := opts.ToStackPreviewMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = c.Post(previewURL(c), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Abandon deletes the stack with the provided stackName and stackID, but leaves its +// resources intact, and returns data describing the stack and its resources. +func Abandon(c *gophercloud.ServiceClient, stackName, stackID string) AbandonResult { + var res AbandonResult + _, res.Err = c.Delete(abandonURL(c, stackName, stackID), &gophercloud.RequestOpts{ + JSONResponse: &res.Body, + OkCodes: []int{200}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/requests_test.go new file mode 100644 index 000000000000..1e32ca2a9e6c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/requests_test.go @@ -0,0 +1,217 @@ +package stacks + +import ( + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestCreateStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t, CreateOutput) + + createOpts := CreateOpts{ + Name: "stackcreated", + Timeout: 60, + Template: ` + { + "stack_name": "postman_stack", + "template": { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type":"OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + } + }`, + DisableRollback: Disable, + } + actual, err := Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + expected := CreateExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestAdoptStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleCreateSuccessfully(t, CreateOutput) + + adoptOpts := AdoptOpts{ + AdoptStackData: `{environment{parameters{}}}`, + Name: "stackcreated", + Timeout: 60, + Template: ` + { + "stack_name": "postman_stack", + "template": { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type":"OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + } + }`, + DisableRollback: Disable, + } + actual, err := Adopt(fake.ServiceClient(), adoptOpts).Extract() + th.AssertNoErr(t, err) + + expected := CreateExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestListStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListSuccessfully(t, FullListOutput) + + count := 0 + err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractStacks(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ListExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestUpdateStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleUpdateSuccessfully(t) + + updateOpts := UpdateOpts{ + Template: ` + { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type":"OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + }`, + } + err := Update(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada", updateOpts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDeleteStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteSuccessfully(t) + + err := Delete(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestPreviewStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePreviewSuccessfully(t, GetOutput) + + previewOpts := PreviewOpts{ + Name: "stackcreated", + Timeout: 60, + Template: ` + { + "stack_name": "postman_stack", + "template": { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type":"OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + } + }`, + DisableRollback: Disable, + } + actual, err := Preview(fake.ServiceClient(), previewOpts).Extract() + th.AssertNoErr(t, err) + + expected := PreviewExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/results.go new file mode 100644 index 000000000000..04d3f8ea964a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/results.go @@ -0,0 +1,309 @@ +package stacks + +import ( + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// CreatedStack represents the object extracted from a Create operation. +type CreatedStack struct { + ID string `mapstructure:"id"` + Links []gophercloud.Link `mapstructure:"links"` +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a CreatedStack object and is called after a +// Create operation. +func (r CreateResult) Extract() (*CreatedStack, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Stack *CreatedStack `mapstructure:"stack"` + } + + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + return res.Stack, nil +} + +// AdoptResult represents the result of an Adopt operation. AdoptResult has the +// same form as CreateResult. +type AdoptResult struct { + CreateResult +} + +// StackPage is a pagination.Pager that is returned from a call to the List function. +type StackPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a ListResult contains no Stacks. +func (r StackPage) IsEmpty() (bool, error) { + stacks, err := ExtractStacks(r) + if err != nil { + return true, err + } + return len(stacks) == 0, nil +} + +// ListedStack represents an element in the slice extracted from a List operation. +type ListedStack struct { + CreationTime time.Time `mapstructure:"-"` + Description string `mapstructure:"description"` + ID string `mapstructure:"id"` + Links []gophercloud.Link `mapstructure:"links"` + Name string `mapstructure:"stack_name"` + Status string `mapstructure:"stack_status"` + StatusReason string `mapstructure:"stack_status_reason"` + UpdatedTime time.Time `mapstructure:"-"` +} + +// ExtractStacks extracts and returns a slice of ListedStack. It is used while iterating +// over a stacks.List call. +func ExtractStacks(page pagination.Page) ([]ListedStack, error) { + casted := page.(StackPage).Body + + var res struct { + Stacks []ListedStack `mapstructure:"stacks"` + } + + err := mapstructure.Decode(page.(StackPage).Body, &res) + if err != nil { + return nil, err + } + + var rawStacks []interface{} + switch casted.(type) { + case map[string]interface{}: + rawStacks = casted.(map[string]interface{})["stacks"].([]interface{}) + case map[string][]interface{}: + rawStacks = casted.(map[string][]interface{})["stacks"] + default: + return res.Stacks, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted)) + } + + for i := range rawStacks { + thisStack := (rawStacks[i]).(map[string]interface{}) + + if t, ok := thisStack["creation_time"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res.Stacks, err + } + res.Stacks[i].CreationTime = creationTime + } + + if t, ok := thisStack["updated_time"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res.Stacks, err + } + res.Stacks[i].UpdatedTime = updatedTime + } + } + + return res.Stacks, nil +} + +// RetrievedStack represents the object extracted from a Get operation. +type RetrievedStack struct { + Capabilities []interface{} `mapstructure:"capabilities"` + CreationTime time.Time `mapstructure:"-"` + Description string `mapstructure:"description"` + DisableRollback bool `mapstructure:"disable_rollback"` + ID string `mapstructure:"id"` + Links []gophercloud.Link `mapstructure:"links"` + NotificationTopics []interface{} `mapstructure:"notification_topics"` + Outputs []map[string]interface{} `mapstructure:"outputs"` + Parameters map[string]string `mapstructure:"parameters"` + Name string `mapstructure:"stack_name"` + Status string `mapstructure:"stack_status"` + StatusReason string `mapstructure:"stack_status_reason"` + TemplateDescription string `mapstructure:"template_description"` + Timeout int `mapstructure:"timeout_mins"` + UpdatedTime time.Time `mapstructure:"-"` +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a RetrievedStack object and is called after a +// Get operation. +func (r GetResult) Extract() (*RetrievedStack, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Stack *RetrievedStack `mapstructure:"stack"` + } + + config := &mapstructure.DecoderConfig{ + Result: &res, + WeaklyTypedInput: true, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return nil, err + } + + if err := decoder.Decode(r.Body); err != nil { + return nil, err + } + + b := r.Body.(map[string]interface{})["stack"].(map[string]interface{}) + + if date, ok := b["creation_time"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.Stack.CreationTime = t + } + + if date, ok := b["updated_time"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.Stack.UpdatedTime = t + } + + return res.Stack, err +} + +// UpdateResult represents the result of a Update operation. +type UpdateResult struct { + gophercloud.ErrResult +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// PreviewedStack represents the result of a Preview operation. +type PreviewedStack struct { + Capabilities []interface{} `mapstructure:"capabilities"` + CreationTime time.Time `mapstructure:"-"` + Description string `mapstructure:"description"` + DisableRollback bool `mapstructure:"disable_rollback"` + ID string `mapstructure:"id"` + Links []gophercloud.Link `mapstructure:"links"` + Name string `mapstructure:"stack_name"` + NotificationTopics []interface{} `mapstructure:"notification_topics"` + Parameters map[string]string `mapstructure:"parameters"` + Resources []map[string]interface{} `mapstructure:"resources"` + Status string `mapstructure:"stack_status"` + StatusReason string `mapstructure:"stack_status_reason"` + TemplateDescription string `mapstructure:"template_description"` + Timeout int `mapstructure:"timeout_mins"` + UpdatedTime time.Time `mapstructure:"-"` +} + +// PreviewResult represents the result of a Preview operation. +type PreviewResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a PreviewedStack object and is called after a +// Preview operation. +func (r PreviewResult) Extract() (*PreviewedStack, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Stack *PreviewedStack `mapstructure:"stack"` + } + + config := &mapstructure.DecoderConfig{ + Result: &res, + WeaklyTypedInput: true, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return nil, err + } + + if err := decoder.Decode(r.Body); err != nil { + return nil, err + } + + b := r.Body.(map[string]interface{})["stack"].(map[string]interface{}) + + if date, ok := b["creation_time"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.Stack.CreationTime = t + } + + if date, ok := b["updated_time"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.Stack.UpdatedTime = t + } + + return res.Stack, err +} + +// AbandonedStack represents the result of an Abandon operation. +type AbandonedStack struct { + Status string `mapstructure:"status"` + Name string `mapstructure:"name"` + Template map[string]interface{} `mapstructure:"template"` + Action string `mapstructure:"action"` + ID string `mapstructure:"id"` + Resources map[string]interface{} `mapstructure:"resources"` +} + +// AbandonResult represents the result of an Abandon operation. +type AbandonResult struct { + gophercloud.Result +} + +// Extract returns a pointer to an AbandonedStack object and is called after an +// Abandon operation. +func (r AbandonResult) Extract() (*AbandonedStack, error) { + if r.Err != nil { + return nil, r.Err + } + + var res AbandonedStack + + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// String converts an AbandonResult to a string. This is useful to when passing +// the result of an Abandon operation to an AdoptOpts AdoptStackData field. +func (r AbandonResult) String() (string, error) { + out, err := json.Marshal(r) + if err != nil { + return "", err + } + return string(out), nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/urls.go new file mode 100644 index 000000000000..3dd2bb32ead5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks/urls.go @@ -0,0 +1,35 @@ +package stacks + +import "github.com/rackspace/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("stacks") +} + +func adoptURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func getURL(c *gophercloud.ServiceClient, name, id string) string { + return c.ServiceURL("stacks", name, id) +} + +func updateURL(c *gophercloud.ServiceClient, name, id string) string { + return getURL(c, name, id) +} + +func deleteURL(c *gophercloud.ServiceClient, name, id string) string { + return getURL(c, name, id) +} + +func previewURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("stacks", "preview") +} + +func abandonURL(c *gophercloud.ServiceClient, name, id string) string { + return c.ServiceURL("stacks", name, id, "abandon") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/doc.go new file mode 100644 index 000000000000..5af0bd62a113 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/doc.go @@ -0,0 +1,8 @@ +// Package stacktemplates provides operations for working with Heat templates. +// A Cloud Orchestration template is a portable file, written in a user-readable +// language, that describes how a set of resources should be assembled and what +// software should be installed in order to produce a working stack. The template +// specifies what resources should be used, what attributes can be set, and other +// parameters that are critical to the successful, repeatable automation of a +// specific application stack. +package stacktemplates diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/fixtures.go new file mode 100644 index 000000000000..71fa80891ea9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/fixtures.go @@ -0,0 +1,118 @@ +package stacktemplates + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +// GetExpected represents the expected object from a Get request. +var GetExpected = &Template{ + Description: "Simple template to test heat commands", + HeatTemplateVersion: "2013-05-23", + Parameters: map[string]interface{}{ + "flavor": map[string]interface{}{ + "default": "m1.tiny", + "type": "string", + }, + }, + Resources: map[string]interface{}{ + "hello_world": map[string]interface{}{ + "type": "OS::Nova::Server", + "properties": map[string]interface{}{ + "key_name": "heat_key", + "flavor": map[string]interface{}{ + "get_param": "flavor", + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", + }, + }, + }, +} + +// GetOutput represents the response body from a Get request. +const GetOutput = ` +{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type": "OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } +}` + +// HandleGetSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/template` +// on the test handler mux that responds with a `Get` response. +func HandleGetSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/template", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} + +// ValidateExpected represents the expected object from a Validate request. +var ValidateExpected = &ValidatedTemplate{ + Description: "Simple template to test heat commands", + Parameters: map[string]interface{}{ + "flavor": map[string]interface{}{ + "Default": "m1.tiny", + "Type": "String", + "NoEcho": "false", + "Description": "", + "Label": "flavor", + }, + }, +} + +// ValidateOutput represents the response body from a Validate request. +const ValidateOutput = ` +{ + "Description": "Simple template to test heat commands", + "Parameters": { + "flavor": { + "Default": "m1.tiny", + "Type": "String", + "NoEcho": "false", + "Description": "", + "Label": "flavor" + } + } +}` + +// HandleValidateSuccessfully creates an HTTP handler at `/validate` +// on the test handler mux that responds with a `Validate` response. +func HandleValidateSuccessfully(t *testing.T, output string) { + th.Mux.HandleFunc("/validate", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, output) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/requests.go new file mode 100644 index 000000000000..ad1e468d1996 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/requests.go @@ -0,0 +1,58 @@ +package stacktemplates + +import ( + "fmt" + + "github.com/rackspace/gophercloud" +) + +// Get retreives data for the given stack template. +func Get(c *gophercloud.ServiceClient, stackName, stackID string) GetResult { + var res GetResult + _, res.Err = c.Request("GET", getURL(c, stackName, stackID), gophercloud.RequestOpts{ + JSONResponse: &res.Body, + }) + return res +} + +// ValidateOptsBuilder describes struct types that can be accepted by the Validate call. +// The ValidateOpts struct in this package does. +type ValidateOptsBuilder interface { + ToStackTemplateValidateMap() (map[string]interface{}, error) +} + +// ValidateOpts specifies the template validation parameters. +type ValidateOpts struct { + Template map[string]interface{} + TemplateURL string +} + +// ToStackTemplateValidateMap assembles a request body based on the contents of a ValidateOpts. +func (opts ValidateOpts) ToStackTemplateValidateMap() (map[string]interface{}, error) { + vo := make(map[string]interface{}) + if opts.Template != nil { + vo["template"] = opts.Template + return vo, nil + } + if opts.TemplateURL != "" { + vo["template_url"] = opts.TemplateURL + return vo, nil + } + return vo, fmt.Errorf("One of Template or TemplateURL is required.") +} + +// Validate validates the given stack template. +func Validate(c *gophercloud.ServiceClient, opts ValidateOptsBuilder) ValidateResult { + var res ValidateResult + + reqBody, err := opts.ToStackTemplateValidateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Post(validateURL(c), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/requests_test.go new file mode 100644 index 000000000000..d31c4ac9a2d0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/requests_test.go @@ -0,0 +1,57 @@ +package stacktemplates + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestGetTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetSuccessfully(t, GetOutput) + + actual, err := Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract() + th.AssertNoErr(t, err) + + expected := GetExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestValidateTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleValidateSuccessfully(t, ValidateOutput) + + opts := ValidateOpts{ + Template: map[string]interface{}{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": map[string]interface{}{ + "flavor": map[string]interface{}{ + "default": "m1.tiny", + "type": "string", + }, + }, + "resources": map[string]interface{}{ + "hello_world": map[string]interface{}{ + "type": "OS::Nova::Server", + "properties": map[string]interface{}{ + "key_name": "heat_key", + "flavor": map[string]interface{}{ + "get_param": "flavor", + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", + }, + }, + }, + }, + } + actual, err := Validate(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := ValidateExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/results.go new file mode 100644 index 000000000000..ac2f24b80b4a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/results.go @@ -0,0 +1,60 @@ +package stacktemplates + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" +) + +// Template represents a stack template. +type Template struct { + Description string `mapstructure:"description"` + HeatTemplateVersion string `mapstructure:"heat_template_version"` + Parameters map[string]interface{} `mapstructure:"parameters"` + Resources map[string]interface{} `mapstructure:"resources"` +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a Template object and is called after a +// Get operation. +func (r GetResult) Extract() (*Template, error) { + if r.Err != nil { + return nil, r.Err + } + + var res Template + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// ValidatedTemplate represents the parsed object returned from a Validate request. +type ValidatedTemplate struct { + Description string + Parameters map[string]interface{} +} + +// ValidateResult represents the result of a Validate operation. +type ValidateResult struct { + gophercloud.Result +} + +// Extract returns a pointer to a ValidatedTemplate object and is called after a +// Validate operation. +func (r ValidateResult) Extract() (*ValidatedTemplate, error) { + if r.Err != nil { + return nil, r.Err + } + + var res ValidatedTemplate + if err := mapstructure.Decode(r.Body, &res); err != nil { + return nil, err + } + + return &res, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/urls.go new file mode 100644 index 000000000000..c30b7ca1afed --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates/urls.go @@ -0,0 +1,11 @@ +package stacktemplates + +import "github.com/rackspace/gophercloud" + +func getURL(c *gophercloud.ServiceClient, stackName, stackID string) string { + return c.ServiceURL("stacks", stackName, stackID, "template") +} + +func validateURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("validate") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version.go new file mode 100644 index 000000000000..b697ba8160ad --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version.go @@ -0,0 +1,114 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/rackspace/gophercloud" +) + +// Version is a supported API version, corresponding to a vN package within the appropriate service. +type Version struct { + ID string + Suffix string + Priority int +} + +var goodStatus = map[string]bool{ + "current": true, + "supported": true, + "stable": true, +} + +// ChooseVersion queries the base endpoint of an API to choose the most recent non-experimental alternative from a service's +// published versions. +// It returns the highest-Priority Version among the alternatives that are provided, as well as its corresponding endpoint. +func ChooseVersion(client *gophercloud.ProviderClient, recognized []*Version) (*Version, string, error) { + type linkResp struct { + Href string `json:"href"` + Rel string `json:"rel"` + } + + type valueResp struct { + ID string `json:"id"` + Status string `json:"status"` + Links []linkResp `json:"links"` + } + + type versionsResp struct { + Values []valueResp `json:"values"` + } + + type response struct { + Versions versionsResp `json:"versions"` + } + + normalize := func(endpoint string) string { + if !strings.HasSuffix(endpoint, "/") { + return endpoint + "/" + } + return endpoint + } + identityEndpoint := normalize(client.IdentityEndpoint) + + // If a full endpoint is specified, check version suffixes for a match first. + for _, v := range recognized { + if strings.HasSuffix(identityEndpoint, v.Suffix) { + return v, identityEndpoint, nil + } + } + + var resp response + _, err := client.Request("GET", client.IdentityBase, gophercloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + + if err != nil { + return nil, "", err + } + + byID := make(map[string]*Version) + for _, version := range recognized { + byID[version.ID] = version + } + + var highest *Version + var endpoint string + + for _, value := range resp.Versions.Values { + href := "" + for _, link := range value.Links { + if link.Rel == "self" { + href = normalize(link.Href) + } + } + + if matching, ok := byID[value.ID]; ok { + // Prefer a version that exactly matches the provided endpoint. + if href == identityEndpoint { + if href == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, client.IdentityBase) + } + return matching, href, nil + } + + // Otherwise, find the highest-priority version with a whitelisted status. + if goodStatus[strings.ToLower(value.Status)] { + if highest == nil || matching.Priority > highest.Priority { + highest = matching + endpoint = href + } + } + } + } + + if highest == nil { + return nil, "", fmt.Errorf("No supported version available from endpoint %s", client.IdentityBase) + } + if endpoint == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, client.IdentityBase) + } + + return highest, endpoint, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version_test.go new file mode 100644 index 000000000000..388d6892cf49 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/openstack/utils/choose_version_test.go @@ -0,0 +1,118 @@ +package utils + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper" +) + +func setupVersionHandler() { + testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "stable", + "id": "v3.0", + "links": [ + { "href": "%s/v3.0", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s/v2.0", "rel": "self" } + ] + } + ] + } + } + `, testhelper.Server.URL, testhelper.Server.URL) + }) +} + +func TestChooseVersion(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + setupVersionHandler() + + v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "blarg"} + v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "hargl"} + + c := &gophercloud.ProviderClient{ + IdentityBase: testhelper.Endpoint(), + IdentityEndpoint: "", + } + v, endpoint, err := ChooseVersion(c, []*Version{v2, v3}) + + if err != nil { + t.Fatalf("Unexpected error from ChooseVersion: %v", err) + } + + if v != v3 { + t.Errorf("Expected %#v to win, but %#v did instead", v3, v) + } + + expected := testhelper.Endpoint() + "v3.0/" + if endpoint != expected { + t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint) + } +} + +func TestChooseVersionOpinionatedLink(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + setupVersionHandler() + + v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "nope"} + v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "northis"} + + c := &gophercloud.ProviderClient{ + IdentityBase: testhelper.Endpoint(), + IdentityEndpoint: testhelper.Endpoint() + "v2.0/", + } + v, endpoint, err := ChooseVersion(c, []*Version{v2, v3}) + if err != nil { + t.Fatalf("Unexpected error from ChooseVersion: %v", err) + } + + if v != v2 { + t.Errorf("Expected %#v to win, but %#v did instead", v2, v) + } + + expected := testhelper.Endpoint() + "v2.0/" + if endpoint != expected { + t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint) + } +} + +func TestChooseVersionFromSuffix(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "/v2.0/"} + v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "/v3.0/"} + + c := &gophercloud.ProviderClient{ + IdentityBase: testhelper.Endpoint(), + IdentityEndpoint: testhelper.Endpoint() + "v2.0/", + } + v, endpoint, err := ChooseVersion(c, []*Version{v2, v3}) + if err != nil { + t.Fatalf("Unexpected error from ChooseVersion: %v", err) + } + + if v != v2 { + t.Errorf("Expected %#v to win, but %#v did instead", v2, v) + } + + expected := testhelper.Endpoint() + "v2.0/" + if endpoint != expected { + t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go new file mode 100644 index 000000000000..cabcccd79f39 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/http.go @@ -0,0 +1,54 @@ +package pagination + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/rackspace/gophercloud" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + gophercloud.Result + url.URL +} + +// PageResultFrom parses an HTTP response as JSON and returns a PageResult containing the +// results, interpreting it as JSON if the content type indicates. +func PageResultFrom(resp *http.Response) (PageResult, error) { + var parsedBody interface{} + + defer resp.Body.Close() + rawBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return PageResult{}, err + } + + if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + err = json.Unmarshal(rawBody, &parsedBody) + if err != nil { + return PageResult{}, err + } + } else { + parsedBody = rawBody + } + + return PageResult{ + Result: gophercloud.Result{ + Body: parsedBody, + Header: resp.Header, + }, + URL: *resp.Request.URL, + }, err +} + +// Request performs an HTTP request and extracts the http.Response from the result. +func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) { + return client.Request("GET", url, gophercloud.RequestOpts{ + MoreHeaders: headers, + OkCodes: []int{200, 204}, + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked.go new file mode 100644 index 000000000000..e9bd8dec9767 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked.go @@ -0,0 +1,67 @@ +package pagination + +import "fmt" + +// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result. +type LinkedPageBase struct { + PageResult + + // LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer. + // If any link along the path is missing, an empty URL will be returned. + // If any link results in an unexpected value type, an error will be returned. + // When left as "nil", []string{"links", "next"} will be used as a default. + LinkPath []string +} + +// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present. +// It assumes that the links are available in a "links" element of the top-level response object. +// If this is not the case, override NextPageURL on your result type. +func (current LinkedPageBase) NextPageURL() (string, error) { + var path []string + var key string + + if current.LinkPath == nil { + path = []string{"links", "next"} + } else { + path = current.LinkPath + } + + submap, ok := current.Body.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("Expected an object, but was %#v", current.Body) + } + + for { + key, path = path[0], path[1:len(path)] + + value, ok := submap[key] + if !ok { + return "", nil + } + + if len(path) > 0 { + submap, ok = value.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("Expected an object, but was %#v", value) + } + } else { + if value == nil { + // Actual null element. + return "", nil + } + + url, ok := value.(string) + if !ok { + return "", fmt.Errorf("Expected a string, but was %#v", value) + } + + return url, nil + } + } +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current LinkedPageBase) GetBody() interface{} { + return current.Body +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked_test.go new file mode 100644 index 000000000000..1ac0f73164f6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/linked_test.go @@ -0,0 +1,120 @@ +package pagination + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud/testhelper" +) + +// LinkedPager sample and test cases. + +type LinkedPageResult struct { + LinkedPageBase +} + +func (r LinkedPageResult) IsEmpty() (bool, error) { + is, err := ExtractLinkedInts(r) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +func ExtractLinkedInts(page Page) ([]int, error) { + var response struct { + Ints []int `mapstructure:"ints"` + } + + err := mapstructure.Decode(page.(LinkedPageResult).Body, &response) + if err != nil { + return nil, err + } + + return response.Ints, nil +} + +func createLinked(t *testing.T) Pager { + testhelper.SetupHTTP() + + testhelper.Mux.HandleFunc("/page1", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [1, 2, 3], "links": { "next": "%s/page2" } }`, testhelper.Server.URL) + }) + + testhelper.Mux.HandleFunc("/page2", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [4, 5, 6], "links": { "next": "%s/page3" } }`, testhelper.Server.URL) + }) + + testhelper.Mux.HandleFunc("/page3", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [7, 8, 9], "links": { "next": null } }`) + }) + + client := createClient() + + createPage := func(r PageResult) Page { + return LinkedPageResult{LinkedPageBase{PageResult: r}} + } + + return NewPager(client, testhelper.Server.URL+"/page1", createPage) +} + +func TestEnumerateLinked(t *testing.T) { + pager := createLinked(t) + defer testhelper.TeardownHTTP() + + callCount := 0 + err := pager.EachPage(func(page Page) (bool, error) { + actual, err := ExtractLinkedInts(page) + if err != nil { + return false, err + } + + t.Logf("Handler invoked with %v", actual) + + var expected []int + switch callCount { + case 0: + expected = []int{1, 2, 3} + case 1: + expected = []int{4, 5, 6} + case 2: + expected = []int{7, 8, 9} + default: + t.Fatalf("Unexpected call count: %d", callCount) + return false, nil + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Call %d: Expected %#v, but was %#v", callCount, expected, actual) + } + + callCount++ + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error for page iteration: %v", err) + } + + if callCount != 3 { + t.Errorf("Expected 3 calls, but was %d", callCount) + } +} + +func TestAllPagesLinked(t *testing.T) { + pager := createLinked(t) + defer testhelper.TeardownHTTP() + + page, err := pager.AllPages() + testhelper.AssertNoErr(t, err) + + expected := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} + actual, err := ExtractLinkedInts(page) + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker.go new file mode 100644 index 000000000000..f355afc54b57 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker.go @@ -0,0 +1,40 @@ +package pagination + +// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager. +// For convenience, embed the MarkedPageBase struct. +type MarkerPage interface { + Page + + // LastMarker returns the last "marker" value on this page. + LastMarker() (string, error) +} + +// MarkerPageBase is a page in a collection that's paginated by "limit" and "marker" query parameters. +type MarkerPageBase struct { + PageResult + + // Owner is a reference to the embedding struct. + Owner MarkerPage +} + +// NextPageURL generates the URL for the page of results after this one. +func (current MarkerPageBase) NextPageURL() (string, error) { + currentURL := current.URL + + mark, err := current.Owner.LastMarker() + if err != nil { + return "", err + } + + q := currentURL.Query() + q.Set("marker", mark) + currentURL.RawQuery = q.Encode() + + return currentURL.String(), nil +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current MarkerPageBase) GetBody() interface{} { + return current.Body +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker_test.go new file mode 100644 index 000000000000..f4d55be810b8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/marker_test.go @@ -0,0 +1,126 @@ +package pagination + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/rackspace/gophercloud/testhelper" +) + +// MarkerPager sample and test cases. + +type MarkerPageResult struct { + MarkerPageBase +} + +func (r MarkerPageResult) IsEmpty() (bool, error) { + results, err := ExtractMarkerStrings(r) + if err != nil { + return true, err + } + return len(results) == 0, err +} + +func (r MarkerPageResult) LastMarker() (string, error) { + results, err := ExtractMarkerStrings(r) + if err != nil { + return "", err + } + if len(results) == 0 { + return "", nil + } + return results[len(results)-1], nil +} + +func createMarkerPaged(t *testing.T) Pager { + testhelper.SetupHTTP() + + testhelper.Mux.HandleFunc("/page", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + ms := r.Form["marker"] + switch { + case len(ms) == 0: + fmt.Fprintf(w, "aaa\nbbb\nccc") + case len(ms) == 1 && ms[0] == "ccc": + fmt.Fprintf(w, "ddd\neee\nfff") + case len(ms) == 1 && ms[0] == "fff": + fmt.Fprintf(w, "ggg\nhhh\niii") + case len(ms) == 1 && ms[0] == "iii": + w.WriteHeader(http.StatusNoContent) + default: + t.Errorf("Request with unexpected marker: [%v]", ms) + } + }) + + client := createClient() + + createPage := func(r PageResult) Page { + p := MarkerPageResult{MarkerPageBase{PageResult: r}} + p.MarkerPageBase.Owner = p + return p + } + + return NewPager(client, testhelper.Server.URL+"/page", createPage) +} + +func ExtractMarkerStrings(page Page) ([]string, error) { + content := page.(MarkerPageResult).Body.([]uint8) + parts := strings.Split(string(content), "\n") + results := make([]string, 0, len(parts)) + for _, part := range parts { + if len(part) > 0 { + results = append(results, part) + } + } + return results, nil +} + +func TestEnumerateMarker(t *testing.T) { + pager := createMarkerPaged(t) + defer testhelper.TeardownHTTP() + + callCount := 0 + err := pager.EachPage(func(page Page) (bool, error) { + actual, err := ExtractMarkerStrings(page) + if err != nil { + return false, err + } + + t.Logf("Handler invoked with %v", actual) + + var expected []string + switch callCount { + case 0: + expected = []string{"aaa", "bbb", "ccc"} + case 1: + expected = []string{"ddd", "eee", "fff"} + case 2: + expected = []string{"ggg", "hhh", "iii"} + default: + t.Fatalf("Unexpected call count: %d", callCount) + return false, nil + } + + testhelper.CheckDeepEquals(t, expected, actual) + + callCount++ + return true, nil + }) + testhelper.AssertNoErr(t, err) + testhelper.AssertEquals(t, callCount, 3) +} + +func TestAllPagesMarker(t *testing.T) { + pager := createMarkerPaged(t) + defer testhelper.TeardownHTTP() + + page, err := pager.AllPages() + testhelper.AssertNoErr(t, err) + + expected := []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii"} + actual, err := ExtractMarkerStrings(page) + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/null.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/null.go new file mode 100644 index 000000000000..ae57e1886c93 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/null.go @@ -0,0 +1,20 @@ +package pagination + +// nullPage is an always-empty page that trivially satisfies all Page interfacts. +// It's useful to be returned along with an error. +type nullPage struct{} + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (p nullPage) NextPageURL() (string, error) { + return "", nil +} + +// IsEmpty always returns true to prevent iteration over nullPages. +func (p nullPage) IsEmpty() (bool, error) { + return true, nil +} + +// LastMark always returns "" because the nullPage contains no items to have a mark. +func (p nullPage) LastMark() (string, error) { + return "", nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go new file mode 100644 index 000000000000..ea47c695dc3d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pager.go @@ -0,0 +1,224 @@ +package pagination + +import ( + "errors" + "fmt" + "net/http" + "reflect" + "strings" + + "github.com/rackspace/gophercloud" +) + +var ( + // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist. + ErrPageNotAvailable = errors.New("The requested page does not exist.") +) + +// Page must be satisfied by the result type of any resource collection. +// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated. +// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs, +// instead. +// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type +// will need to implement. +type Page interface { + + // NextPageURL generates the URL for the page of data that follows this collection. + // Return "" if no such page exists. + NextPageURL() (string, error) + + // IsEmpty returns true if this Page has no items in it. + IsEmpty() (bool, error) + + // GetBody returns the Page Body. This is used in the `AllPages` method. + GetBody() interface{} +} + +// Pager knows how to advance through a specific resource collection, one page at a time. +type Pager struct { + client *gophercloud.ServiceClient + + initialURL string + + createPage func(r PageResult) Page + + Err error + + // Headers supplies additional HTTP headers to populate on each paged request. + Headers map[string]string +} + +// NewPager constructs a manually-configured pager. +// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page. +func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager { + return Pager{ + client: client, + initialURL: initialURL, + createPage: createPage, + } +} + +// WithPageCreator returns a new Pager that substitutes a different page creation function. This is +// useful for overriding List functions in delegation. +func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager { + return Pager{ + client: p.client, + initialURL: p.initialURL, + createPage: createPage, + } +} + +func (p Pager) fetchNextPage(url string) (Page, error) { + resp, err := Request(p.client, p.Headers, url) + if err != nil { + return nil, err + } + + remembered, err := PageResultFrom(resp) + if err != nil { + return nil, err + } + + return p.createPage(remembered), nil +} + +// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function. +// Return "false" from the handler to prematurely stop iterating. +func (p Pager) EachPage(handler func(Page) (bool, error)) error { + if p.Err != nil { + return p.Err + } + currentURL := p.initialURL + for { + currentPage, err := p.fetchNextPage(currentURL) + if err != nil { + return err + } + + empty, err := currentPage.IsEmpty() + if err != nil { + return err + } + if empty { + return nil + } + + ok, err := handler(currentPage) + if err != nil { + return err + } + if !ok { + return nil + } + + currentURL, err = currentPage.NextPageURL() + if err != nil { + return err + } + if currentURL == "" { + return nil + } + } +} + +// AllPages returns all the pages from a `List` operation in a single page, +// allowing the user to retrieve all the pages at once. +func (p Pager) AllPages() (Page, error) { + // pagesSlice holds all the pages until they get converted into as Page Body. + var pagesSlice []interface{} + // body will contain the final concatenated Page body. + var body reflect.Value + + // Grab a test page to ascertain the page body type. + testPage, err := p.fetchNextPage(p.initialURL) + if err != nil { + return nil, err + } + // Store the page type so we can use reflection to create a new mega-page of + // that type. + pageType := reflect.TypeOf(testPage) + + // Switch on the page body type. Recognized types are `map[string]interface{}`, + // `[]byte`, and `[]interface{}`. + switch testPage.GetBody().(type) { + case map[string]interface{}: + // key is the map key for the page body if the body type is `map[string]interface{}`. + var key string + // Iterate over the pages to concatenate the bodies. + err := p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().(map[string]interface{}) + for k := range b { + // If it's a linked page, we don't want the `links`, we want the other one. + if !strings.HasSuffix(k, "links") { + key = k + } + } + pagesSlice = append(pagesSlice, b[key].([]interface{})...) + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `map[string]interface{}` + body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice))) + body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice)) + case []byte: + // Iterate over the pages to concatenate the bodies. + err := p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().([]byte) + pagesSlice = append(pagesSlice, b) + // seperate pages with a comma + pagesSlice = append(pagesSlice, []byte{10}) + return true, nil + }) + if err != nil { + return nil, err + } + // Remove the trailing comma. + pagesSlice = pagesSlice[:len(pagesSlice)-1] + var b []byte + // Combine the slice of slices in to a single slice. + for _, slice := range pagesSlice { + b = append(b, slice.([]byte)...) + } + // Set body to value of type `bytes`. + body = reflect.New(reflect.TypeOf(b)).Elem() + body.SetBytes(b) + case []interface{}: + // Iterate over the pages to concatenate the bodies. + err := p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().([]interface{}) + pagesSlice = append(pagesSlice, b...) + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `[]interface{}` + body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice)) + for i, s := range pagesSlice { + body.Index(i).Set(reflect.ValueOf(s)) + } + default: + return nil, fmt.Errorf("Page body has unrecognized type.") + } + + // Each `Extract*` function is expecting a specific type of page coming back, + // otherwise the type assertion in those functions will fail. pageType is needed + // to create a type in this method that has the same type that the `Extract*` + // function is expecting and set the Body of that object to the concatenated + // pages. + page := reflect.New(pageType) + // Set the page body to be the concatenated pages. + page.Elem().FieldByName("Body").Set(body) + // Set any additional headers that were pass along. The `objectstorage` pacakge, + // for example, passes a Content-Type header. + h := make(http.Header) + for k, v := range p.Headers { + h.Add(k, v) + } + page.Elem().FieldByName("Header").Set(reflect.ValueOf(h)) + // Type assert the page to a Page interface so that the type assertion in the + // `Extract*` methods will work. + return page.Elem().Interface().(Page), err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pagination_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pagination_test.go new file mode 100644 index 000000000000..f3e4de1b042d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pagination_test.go @@ -0,0 +1,13 @@ +package pagination + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper" +) + +func createClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{TokenID: "abc123"}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pkg.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pkg.go new file mode 100644 index 000000000000..912daea36426 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/pkg.go @@ -0,0 +1,4 @@ +/* +Package pagination contains utilities and convenience structs that implement common pagination idioms within OpenStack APIs. +*/ +package pagination diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single.go new file mode 100644 index 000000000000..f78d4ab5d926 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single.go @@ -0,0 +1,15 @@ +package pagination + +// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once. +type SinglePageBase PageResult + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (current SinglePageBase) NextPageURL() (string, error) { + return "", nil +} + +// GetBody returns the single page's body. This method is needed to satisfy the +// Page interface. +func (current SinglePageBase) GetBody() interface{} { + return current.Body +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single_test.go new file mode 100644 index 000000000000..4af0fee69ab4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/pagination/single_test.go @@ -0,0 +1,84 @@ +package pagination + +import ( + "fmt" + "net/http" + "testing" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud/testhelper" +) + +// SinglePage sample and test cases. + +type SinglePageResult struct { + SinglePageBase +} + +func (r SinglePageResult) IsEmpty() (bool, error) { + is, err := ExtractSingleInts(r) + if err != nil { + return true, err + } + return len(is) == 0, nil +} + +func ExtractSingleInts(page Page) ([]int, error) { + var response struct { + Ints []int `mapstructure:"ints"` + } + + err := mapstructure.Decode(page.(SinglePageResult).Body, &response) + if err != nil { + return nil, err + } + + return response.Ints, nil +} + +func setupSinglePaged() Pager { + testhelper.SetupHTTP() + client := createClient() + + testhelper.Mux.HandleFunc("/only", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{ "ints": [1, 2, 3] }`) + }) + + createPage := func(r PageResult) Page { + return SinglePageResult{SinglePageBase(r)} + } + + return NewPager(client, testhelper.Server.URL+"/only", createPage) +} + +func TestEnumerateSinglePaged(t *testing.T) { + callCount := 0 + pager := setupSinglePaged() + defer testhelper.TeardownHTTP() + + err := pager.EachPage(func(page Page) (bool, error) { + callCount++ + + expected := []int{1, 2, 3} + actual, err := ExtractSingleInts(page) + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, expected, actual) + return true, nil + }) + testhelper.CheckNoErr(t, err) + testhelper.CheckEquals(t, 1, callCount) +} + +func TestAllPagesSingle(t *testing.T) { + pager := setupSinglePaged() + defer testhelper.TeardownHTTP() + + page, err := pager.AllPages() + testhelper.AssertNoErr(t, err) + + expected := []int{1, 2, 3} + actual, err := ExtractSingleInts(page) + testhelper.AssertNoErr(t, err) + testhelper.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go new file mode 100644 index 000000000000..4d0f1e6e0281 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params.go @@ -0,0 +1,271 @@ +package gophercloud + +import ( + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +// 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 + } + return nil +} + +/* +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 + } + return nil +} + +var t time.Time + +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + z := true + for i := 0; i < v.Len(); i++ { + z = z && isZero(v.Index(i)) + } + return z + case reflect.Struct: + if v.Type() == reflect.TypeOf(t) { + if v.Interface().(time.Time).IsZero() { + return true + } + return false + } + z := true + for i := 0; i < v.NumField(); i++ { + z = z && isZero(v.Field(i)) + } + return z + } + // Compare other types directly: + z := reflect.Zero(v.Type()) + return v.Interface() == z.Interface() +} + +/* +BuildQueryString is an internal function to be used by request methods in +individual resource packages. + +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"` + } + + 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) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + params := url.Values{} + + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + qTag := f.Tag.Get("q") + + // if the field has a 'q' tag, it goes in the query string + if qTag != "" { + tags := strings.Split(qTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + switch v.Kind() { + case reflect.String: + params.Add(tags[0], v.String()) + case reflect.Int: + params.Add(tags[0], strconv.FormatInt(v.Int(), 10)) + case reflect.Bool: + params.Add(tags[0], strconv.FormatBool(v.Bool())) + case reflect.Slice: + switch v.Type().Elem() { + case reflect.TypeOf(0): + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], strconv.FormatInt(v.Index(i).Int(), 10)) + } + default: + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], v.Index(i).String()) + } + } + } + } else { + // Otherwise, the field is not set. + if len(tags) == 2 && tags[1] == "required" { + // And the field is required. Return an error. + return nil, fmt.Errorf("Required query parameter [%s] not set.", f.Name) + } + } + } + } + + 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 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 { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + optsMap := make(map[string]string) + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + hTag := f.Tag.Get("h") + + // if the field has a 'h' tag, it goes in the header + if hTag != "" { + tags := strings.Split(hTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + switch v.Kind() { + case reflect.String: + optsMap[tags[0]] = v.String() + case reflect.Int: + optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10) + case reflect.Bool: + optsMap[tags[0]] = strconv.FormatBool(v.Bool()) + } + } else { + // Otherwise, the field is not set. + if len(tags) == 2 && tags[1] == "required" { + // And the field is required. Return an error. + return optsMap, fmt.Errorf("Required header not set.") + } + } + } + + } + return optsMap, nil + } + // 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 new file mode 100644 index 000000000000..2f40eec81222 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/params_test.go @@ -0,0 +1,165 @@ +package gophercloud + +import ( + "net/url" + "reflect" + "testing" + "time" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestMaybeString(t *testing.T) { + testString := "" + var expected *string + actual := MaybeString(testString) + th.CheckDeepEquals(t, expected, actual) + + testString = "carol" + expected = &testString + actual = MaybeString(testString) + th.CheckDeepEquals(t, expected, actual) +} + +func TestMaybeInt(t *testing.T) { + testInt := 0 + var expected *int + actual := MaybeInt(testInt) + th.CheckDeepEquals(t, expected, actual) + + testInt = 4 + expected = &testInt + actual = MaybeInt(testInt) + th.CheckDeepEquals(t, expected, actual) +} + +func TestBuildQueryString(t *testing.T) { + type testVar string + opts := struct { + J int `q:"j"` + R string `q:"r,required"` + C bool `q:"c"` + S []string `q:"s"` + TS []testVar `q:"ts"` + TI []int `q:"ti"` + }{ + J: 2, + R: "red", + C: true, + S: []string{"one", "two", "three"}, + TS: []testVar{"a", "b"}, + TI: []int{1, 2}, + } + expected := &url.URL{RawQuery: "c=true&j=2&r=red&s=one&s=two&s=three&ti=1&ti=2&ts=a&ts=b"} + actual, err := BuildQueryString(&opts) + if err != nil { + t.Errorf("Error building query string: %v", err) + } + th.CheckDeepEquals(t, expected, actual) + + opts = struct { + J int `q:"j"` + R string `q:"r,required"` + C bool `q:"c"` + S []string `q:"s"` + TS []testVar `q:"ts"` + TI []int `q:"ti"` + }{ + J: 2, + C: true, + } + _, err = BuildQueryString(&opts) + if err == nil { + t.Errorf("Expected error: 'Required field not set'") + } + th.CheckDeepEquals(t, expected, actual) + + _, err = BuildQueryString(map[string]interface{}{"Number": 4}) + if err == nil { + t.Errorf("Expected error: 'Options type is not a struct'") + } +} + +func TestBuildHeaders(t *testing.T) { + testStruct := struct { + Accept string `h:"Accept"` + Num int `h:"Number,required"` + Style bool `h:"Style"` + }{ + Accept: "application/json", + Num: 4, + Style: true, + } + expected := map[string]string{"Accept": "application/json", "Number": "4", "Style": "true"} + actual, err := BuildHeaders(&testStruct) + th.CheckNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) + + testStruct.Num = 0 + _, err = BuildHeaders(&testStruct) + if err == nil { + t.Errorf("Expected error: 'Required header not set'") + } + + _, err = BuildHeaders(map[string]interface{}{"Number": 4}) + if err == nil { + t.Errorf("Expected error: 'Options type is not a struct'") + } +} + +func TestIsZero(t *testing.T) { + var testMap map[string]interface{} + testMapValue := reflect.ValueOf(testMap) + expected := true + actual := isZero(testMapValue) + th.CheckEquals(t, expected, actual) + testMap = map[string]interface{}{"empty": false} + testMapValue = reflect.ValueOf(testMap) + expected = false + actual = isZero(testMapValue) + th.CheckEquals(t, expected, actual) + + var testArray [2]string + testArrayValue := reflect.ValueOf(testArray) + expected = true + actual = isZero(testArrayValue) + th.CheckEquals(t, expected, actual) + testArray = [2]string{"one", "two"} + testArrayValue = reflect.ValueOf(testArray) + expected = false + actual = isZero(testArrayValue) + th.CheckEquals(t, expected, actual) + + var testStruct struct { + A string + B time.Time + } + testStructValue := reflect.ValueOf(testStruct) + expected = true + actual = isZero(testStructValue) + th.CheckEquals(t, expected, actual) + testStruct = struct { + A string + B time.Time + }{ + B: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), + } + testStructValue = reflect.ValueOf(testStruct) + 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/provider_client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go new file mode 100644 index 000000000000..0dff2cfc3033 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client.go @@ -0,0 +1,300 @@ +package gophercloud + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "strings" +) + +// DefaultUserAgent is the default User-Agent string set in the request header. +const DefaultUserAgent = "gophercloud/1.0.0" + +// UserAgent represents a User-Agent header. +type UserAgent struct { + // prepend is the slice of User-Agent strings to prepend to DefaultUserAgent. + // All the strings to prepend are accumulated and prepended in the Join method. + prepend []string +} + +// Prepend prepends a user-defined string to the default User-Agent string. Users +// may pass in one or more strings to prepend. +func (ua *UserAgent) Prepend(s ...string) { + ua.prepend = append(s, ua.prepend...) +} + +// Join concatenates all the user-defined User-Agend strings with the default +// Gophercloud User-Agent string. +func (ua *UserAgent) Join() string { + uaSlice := append(ua.prepend, DefaultUserAgent) + return strings.Join(uaSlice, " ") +} + +// ProviderClient stores details that are required to interact with any +// services within a specific provider's API. +// +// Generally, you acquire a ProviderClient by calling the NewClient method in +// the appropriate provider's child package, providing whatever authentication +// credentials are required. +type ProviderClient struct { + // IdentityBase is the base URL used for a particular provider's identity + // service - it will be used when issuing authenticatation requests. It + // should point to the root resource of the identity service, not a specific + // identity version. + IdentityBase string + + // IdentityEndpoint is the identity endpoint. This may be a specific version + // of the identity service. If this is the case, this endpoint is used rather + // than querying versions first. + IdentityEndpoint string + + // TokenID is the ID of the most recently issued valid token. + TokenID string + + // EndpointLocator describes how this provider discovers the endpoints for + // its constituent services. + EndpointLocator EndpointLocator + + // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors. + HTTPClient http.Client + + // UserAgent represents the User-Agent header in the HTTP request. + UserAgent UserAgent + + // ReauthFunc is the function used to re-authenticate the user if the request + // fails with a 401 HTTP response code. This a needed because there may be multiple + // authentication functions for different Identity service versions. + ReauthFunc func() error +} + +// AuthenticatedHeaders returns a map of HTTP headers that are common for all +// authenticated service requests. +func (client *ProviderClient) AuthenticatedHeaders() map[string]string { + if client.TokenID == "" { + return map[string]string{} + } + return map[string]string{"X-Auth-Token": client.TokenID} +} + +// RequestOpts customizes the behavior of the provider.Request() method. +type RequestOpts struct { + // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The + // content type of the request will default to "application/json" unless overridden by MoreHeaders. + // It's an error to specify both a JSONBody and a RawBody. + JSONBody interface{} + // RawBody contains an io.Reader that will be consumed by the request directly. No content-type + // will be set unless one is provided explicitly by MoreHeaders. + RawBody io.Reader + + // JSONResponse, if provided, will be populated with the contents of the response body parsed as + // JSON. + JSONResponse interface{} + // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If + // the response has a different code, an error will be returned. + OkCodes []int + + // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is + // provided with a blank value (""), that header will be *omitted* instead: use this to suppress + // the default Accept header or an inferred Content-Type, for example. + MoreHeaders map[string]string +} + +// UnexpectedResponseCodeError is returned by the Request method when a response code other than +// those listed in OkCodes is encountered. +type UnexpectedResponseCodeError struct { + URL string + Method string + Expected []int + Actual int + Body []byte +} + +func (err *UnexpectedResponseCodeError) Error() string { + return fmt.Sprintf( + "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s", + err.Expected, err.Method, err.URL, err.Actual, err.Body, + ) +} + +var applicationJSON = "application/json" + +// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication +// header will automatically be provided. +func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) { + var body io.Reader + var contentType *string + + // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided + // io.Reader as-is. Default the content-type to application/json. + if options.JSONBody != nil { + if options.RawBody != nil { + panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().") + } + + rendered, err := json.Marshal(options.JSONBody) + if err != nil { + return nil, err + } + + body = bytes.NewReader(rendered) + contentType = &applicationJSON + } + + if options.RawBody != nil { + body = options.RawBody + } + + // Construct the http.Request. + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to + // modify or omit any header. + if contentType != nil { + req.Header.Set("Content-Type", *contentType) + } + req.Header.Set("Accept", applicationJSON) + + for k, v := range client.AuthenticatedHeaders() { + req.Header.Add(k, v) + } + + // Set the User-Agent header + req.Header.Set("User-Agent", client.UserAgent.Join()) + + if options.MoreHeaders != nil { + for k, v := range options.MoreHeaders { + if v != "" { + req.Header.Set(k, v) + } else { + req.Header.Del(k) + } + } + } + + // Issue the request. + resp, err := client.HTTPClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusUnauthorized { + if client.ReauthFunc != nil { + err = client.ReauthFunc() + if err != nil { + return nil, fmt.Errorf("Error trying to re-authenticate: %s", err) + } + resp, err = client.Request(method, url, options) + if err != nil { + return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err) + } + } + } + + // Allow default OkCodes if none explicitly set + if options.OkCodes == nil { + options.OkCodes = defaultOkCodes(method) + } + + // Validate the HTTP response status. + var ok bool + for _, code := range options.OkCodes { + if resp.StatusCode == code { + ok = true + break + } + } + if !ok { + body, _ := ioutil.ReadAll(resp.Body) + resp.Body.Close() + return resp, &UnexpectedResponseCodeError{ + URL: url, + Method: method, + Expected: options.OkCodes, + Actual: resp.StatusCode, + Body: body, + } + } + + // Parse the response body as JSON, if requested to do so. + if options.JSONResponse != nil { + defer resp.Body.Close() + json.NewDecoder(resp.Body).Decode(options.JSONResponse) + } + + return resp, nil +} + +func defaultOkCodes(method string) []int { + switch { + case method == "GET": + return []int{200} + case method == "POST": + return []int{201, 202} + case method == "PUT": + return []int{201, 202} + case method == "DELETE": + return []int{202, 204} + } + + return []int{} +} + +func (client *ProviderClient) Get(url string, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = &RequestOpts{} + } + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + return client.Request("GET", url, *opts) +} + +func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = &RequestOpts{} + } + + if v, ok := (JSONBody).(io.Reader); ok { + opts.RawBody = v + } else if JSONBody != nil { + opts.JSONBody = JSONBody + } + + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + + return client.Request("POST", url, *opts) +} + +func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = &RequestOpts{} + } + + if v, ok := (JSONBody).(io.Reader); ok { + opts.RawBody = v + } else if JSONBody != nil { + opts.JSONBody = JSONBody + } + + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + + return client.Request("PUT", url, *opts) +} + +func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = &RequestOpts{} + } + + return client.Request("DELETE", url, *opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client_test.go new file mode 100644 index 000000000000..d79d862b2cf9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/provider_client_test.go @@ -0,0 +1,35 @@ +package gophercloud + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticatedHeaders(t *testing.T) { + p := &ProviderClient{ + TokenID: "1234", + } + expected := map[string]string{"X-Auth-Token": "1234"} + actual := p.AuthenticatedHeaders() + th.CheckDeepEquals(t, expected, actual) +} + +func TestUserAgent(t *testing.T) { + p := &ProviderClient{} + + p.UserAgent.Prepend("custom-user-agent/2.4.0") + expected := "custom-user-agent/2.4.0 gophercloud/1.0.0" + actual := p.UserAgent.Join() + th.CheckEquals(t, expected, actual) + + p.UserAgent.Prepend("another-custom-user-agent/0.3.0", "a-third-ua/5.9.0") + expected = "another-custom-user-agent/0.3.0 a-third-ua/5.9.0 custom-user-agent/2.4.0 gophercloud/1.0.0" + actual = p.UserAgent.Join() + th.CheckEquals(t, expected, actual) + + p.UserAgent = UserAgent{} + expected = "gophercloud/1.0.0" + actual = p.UserAgent.Join() + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/auth_env.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/auth_env.go new file mode 100644 index 000000000000..5852c3ce7384 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/auth_env.go @@ -0,0 +1,57 @@ +package rackspace + +import ( + "fmt" + "os" + + "github.com/rackspace/gophercloud" +) + +var nilOptions = gophercloud.AuthOptions{} + +// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the +// required RS_AUTH_URL, RS_USERNAME, or RS_PASSWORD environment variables, +// respectively, remain undefined. See the AuthOptions() function for more details. +var ( + ErrNoAuthURL = fmt.Errorf("Environment variable RS_AUTH_URL or OS_AUTH_URL need to be set.") + ErrNoUsername = fmt.Errorf("Environment variable RS_USERNAME or OS_USERNAME need to be set.") + ErrNoPassword = fmt.Errorf("Environment variable RS_API_KEY or RS_PASSWORD needs to be set.") +) + +func prefixedEnv(base string) string { + value := os.Getenv("RS_" + base) + if value == "" { + value = os.Getenv("OS_" + base) + } + return value +} + +// AuthOptionsFromEnv fills out an identity.AuthOptions structure with the +// settings found on the various Rackspace RS_* environment variables. +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := prefixedEnv("AUTH_URL") + username := prefixedEnv("USERNAME") + password := prefixedEnv("PASSWORD") + apiKey := prefixedEnv("API_KEY") + + if authURL == "" { + return nilOptions, ErrNoAuthURL + } + + if username == "" { + return nilOptions, ErrNoUsername + } + + if password == "" && apiKey == "" { + return nilOptions, ErrNoPassword + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + Username: username, + Password: password, + APIKey: apiKey, + } + + return ao, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate.go new file mode 100644 index 000000000000..1cd1b6e30cc7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate.go @@ -0,0 +1,131 @@ +package snapshots + +import ( + "errors" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" +) + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSnapshotCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Snapshot. This object is passed to +// the snapshots.Create function. For more information about these parameters, +// see the Snapshot object. +type CreateOpts struct { + // REQUIRED + VolumeID string + // OPTIONAL + Description string + // OPTIONAL + Force bool + // OPTIONAL + Name string +} + +// ToSnapshotCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.VolumeID == "" { + return nil, errors.New("Required CreateOpts field 'VolumeID' not set.") + } + + s["volume_id"] = opts.VolumeID + + if opts.Description != "" { + s["display_description"] = opts.Description + } + if opts.Name != "" { + s["display_name"] = opts.Name + } + if opts.Force { + s["force"] = opts.Force + } + + return map[string]interface{}{"snapshot": s}, nil +} + +// Create will create a new Snapshot based on the values in CreateOpts. To +// extract the Snapshot object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + return CreateResult{os.Create(client, opts)} +} + +// Delete will delete the existing Snapshot with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(client, id) +} + +// Get retrieves the Snapshot with the provided ID. To extract the Snapshot +// object from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + return GetResult{os.Get(client, id)} +} + +// List returns Snapshots. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client, os.ListOpts{}) +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update 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 UpdateOptsBuilder interface { + ToSnapshotUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + Name string + Description string +} + +// ToSnapshotUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToSnapshotUpdateMap() (map[string]interface{}, error) { + s := make(map[string]interface{}) + + if opts.Name != "" { + s["display_name"] = opts.Name + } + if opts.Description != "" { + s["display_description"] = opts.Description + } + + return map[string]interface{}{"snapshot": s}, nil +} + +// Update accepts a UpdateOpts struct and updates an existing snapshot using the +// values provided. +func Update(c *gophercloud.ServiceClient, snapshotID string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToSnapshotUpdateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = c.Request("PUT", updateURL(c, snapshotID), gophercloud.RequestOpts{ + JSONBody: &reqBody, + JSONResponse: &res.Body, + OkCodes: []int{200, 201}, + }) + + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate_test.go new file mode 100644 index 000000000000..1a02b465279d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/delegate_test.go @@ -0,0 +1,97 @@ +package snapshots + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +const endpoint = "http://localhost:57909/v1/12345" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestUpdateURL(t *testing.T) { + actual := updateURL(endpointClient(), "foo") + expected := endpoint + "snapshots/foo" + th.AssertEquals(t, expected, actual) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockListResponse(t) + + count := 0 + + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractSnapshots(page) + if err != nil { + t.Errorf("Failed to extract snapshots: %v", err) + return false, err + } + + expected := []Snapshot{ + Snapshot{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "snapshot-001", + }, + Snapshot{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "snapshot-002", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertEquals(t, 1, count) + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockGetResponse(t) + + v, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "snapshot-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockCreateResponse(t) + + options := &CreateOpts{VolumeID: "1234", Name: "snapshot-001"} + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.VolumeID, "1234") + th.AssertEquals(t, n.Name, "snapshot-001") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockDeleteResponse(t) + + res := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/doc.go new file mode 100644 index 000000000000..ad6064f2af14 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/doc.go @@ -0,0 +1,3 @@ +// Package snapshots provides information and interaction with the snapshot +// API resource for the Rackspace Block Storage service. +package snapshots diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/results.go new file mode 100644 index 000000000000..c81644c5ddd5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots/results.go @@ -0,0 +1,147 @@ +package snapshots + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Status is the type used to represent a snapshot's status +type Status string + +// Constants to use for supported statuses +const ( + Creating Status = "CREATING" + Available Status = "AVAILABLE" + Deleting Status = "DELETING" + Error Status = "ERROR" + DeleteError Status = "ERROR_DELETING" +) + +// Snapshot is the Rackspace representation of an external block storage device. +type Snapshot struct { + // The timestamp when this snapshot was created. + CreatedAt string `mapstructure:"created_at"` + + // The human-readable description for this snapshot. + Description string `mapstructure:"display_description"` + + // The human-readable name for this snapshot. + Name string `mapstructure:"display_name"` + + // The UUID for this snapshot. + ID string `mapstructure:"id"` + + // The random metadata associated with this snapshot. Note: unlike standard + // OpenStack snapshots, this cannot actually be set. + Metadata map[string]string `mapstructure:"metadata"` + + // Indicates the current progress of the snapshot's backup procedure. + Progress string `mapstructure:"os-extended-snapshot-attributes:progress"` + + // The project ID. + ProjectID string `mapstructure:"os-extended-snapshot-attributes:project_id"` + + // The size of the volume which this snapshot backs up. + Size int `mapstructure:"size"` + + // The status of the snapshot. + Status Status `mapstructure:"status"` + + // The ID of the volume which this snapshot seeks to back up. + VolumeID string `mapstructure:"volume_id"` +} + +// 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 { + gophercloud.Result +} + +func commonExtract(resp interface{}, err error) (*Snapshot, error) { + if err != nil { + return nil, err + } + + var respStruct struct { + Snapshot *Snapshot `json:"snapshot"` + } + + err = mapstructure.Decode(resp, &respStruct) + + return respStruct.Snapshot, err +} + +// Extract will get the Snapshot object out of the GetResult object. +func (r GetResult) Extract() (*Snapshot, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Snapshot object out of the CreateResult object. +func (r CreateResult) Extract() (*Snapshot, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Snapshot object out of the UpdateResult object. +func (r UpdateResult) Extract() (*Snapshot, error) { + return commonExtract(r.Body, r.Err) +} + +// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call. +func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) { + var response struct { + Snapshots []Snapshot `json:"snapshots"` + } + + err := mapstructure.Decode(page.(os.ListResult).Body, &response) + return response.Snapshots, err +} + +// WaitUntilComplete will continually poll a snapshot until it successfully +// transitions to a specified state. It will do this for at most the number of +// seconds specified. +func (snapshot Snapshot) WaitUntilComplete(c *gophercloud.ServiceClient, timeout int) error { + return gophercloud.WaitFor(timeout, func() (bool, error) { + // Poll resource + current, err := Get(c, snapshot.ID).Extract() + if err != nil { + return false, err + } + + // Has it been built yet? + if current.Progress == "100%" { + return true, nil + } + + return false, nil + }) +} + +// WaitUntilDeleted will continually poll a snapshot until it has been +// successfully deleted, i.e. returns a 404 status. +func (snapshot Snapshot) WaitUntilDeleted(c *gophercloud.ServiceClient, timeout int) error { + return gophercloud.WaitFor(timeout, func() (bool, error) { + // Poll resource + _, err := Get(c, snapshot.ID).Extract() + + // Check for a 404 + if casted, ok := err.(*gophercloud.UnexpectedResponseCodeError); ok && casted.Actual == 404 { + return true, nil + } else if err != nil { + return false, err + } + + return false, nil + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate.go new file mode 100644 index 000000000000..438349410a6d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate.go @@ -0,0 +1,75 @@ +package volumes + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" +) + +type CreateOpts struct { + os.CreateOpts +} + +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + if opts.Size < 75 || opts.Size > 1024 { + return nil, fmt.Errorf("Size field must be between 75 and 1024") + } + + return opts.CreateOpts.ToVolumeCreateMap() +} + +// Create will create a new Volume based on the values in CreateOpts. To extract +// the Volume object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult { + return CreateResult{os.Create(client, opts)} +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult { + return os.Delete(client, id) +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + return GetResult{os.Get(client, id)} +} + +// List returns volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client, os.ListOpts{}) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + // OPTIONAL + Name string + // OPTIONAL + Description string +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + v := make(map[string]interface{}) + + if opts.Description != "" { + v["display_description"] = opts.Description + } + if opts.Name != "" { + v["display_name"] = opts.Name + } + + return map[string]interface{}{"volume": v}, nil +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) UpdateResult { + return UpdateResult{os.Update(client, id, opts)} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go new file mode 100644 index 000000000000..b44564cc1f6e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/delegate_test.go @@ -0,0 +1,106 @@ +package volumes + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "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.MockListResponse(t) + + count := 0 + + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVolumes(page) + if err != nil { + t.Errorf("Failed to extract volumes: %v", err) + return false, err + } + + expected := []Volume{ + Volume{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-001", + }, + Volume{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-002", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertEquals(t, 1, count) + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockGetResponse(t) + + v, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, v.Name, "vol-001") + th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockCreateResponse(t) + + n, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 75}}).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Size, 4) + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestSizeRange(t *testing.T) { + _, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 1}}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } + + _, err = Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 2000}}).Extract() + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockDeleteResponse(t) + + res := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22") + th.AssertNoErr(t, res.Err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockUpdateResponse(t) + + options := &UpdateOpts{Name: "vol-002"} + v, err := Update(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract() + th.AssertNoErr(t, err) + th.CheckEquals(t, "vol-002", v.Name) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/doc.go new file mode 100644 index 000000000000..b2be25c53817 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/doc.go @@ -0,0 +1,3 @@ +// Package volumes provides information and interaction with the volume +// API resource for the Rackspace Block Storage service. +package volumes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/results.go new file mode 100644 index 000000000000..c7c2cc498412 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes/results.go @@ -0,0 +1,66 @@ +package volumes + +import ( + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Volume wraps an Openstack volume +type Volume os.Volume + +// 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) (*Volume, error) { + if err != nil { + return nil, err + } + + var respStruct struct { + Volume *Volume `json:"volume"` + } + + err = mapstructure.Decode(resp, &respStruct) + + return respStruct.Volume, err +} + +// Extract will get the Volume object out of the GetResult object. +func (r GetResult) Extract() (*Volume, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Volume object out of the CreateResult object. +func (r CreateResult) Extract() (*Volume, error) { + return commonExtract(r.Body, r.Err) +} + +// Extract will get the Volume object out of the UpdateResult object. +func (r UpdateResult) Extract() (*Volume, error) { + return commonExtract(r.Body, r.Err) +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(page pagination.Page) ([]Volume, error) { + var response struct { + Volumes []Volume `json:"volumes"` + } + + err := mapstructure.Decode(page.(os.ListResult).Body, &response) + + return response.Volumes, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate.go new file mode 100644 index 000000000000..c96b3e4a357b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate.go @@ -0,0 +1,18 @@ +package volumetypes + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns all volume types. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client) +} + +// Get will retrieve the volume type with the provided ID. To extract the volume +// type from the result, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + return GetResult{os.Get(client, id)} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate_test.go new file mode 100644 index 000000000000..6e65c904b520 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/delegate_test.go @@ -0,0 +1,64 @@ +package volumetypes + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "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.MockListResponse(t) + + count := 0 + + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVolumeTypes(page) + if err != nil { + t.Errorf("Failed to extract volume types: %v", err) + return false, err + } + + expected := []VolumeType{ + VolumeType{ + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "vol-type-001", + ExtraSpecs: map[string]interface{}{ + "capabilities": "gpu", + }, + }, + VolumeType{ + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "vol-type-002", + ExtraSpecs: map[string]interface{}{}, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + th.AssertEquals(t, 1, count) + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.MockGetResponse(t) + + vt, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"}) + th.AssertEquals(t, vt.Name, "vol-type-001") + th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/doc.go new file mode 100644 index 000000000000..70122b77c4a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/doc.go @@ -0,0 +1,3 @@ +// Package volumetypes provides information and interaction with the volume type +// API resource for the Rackspace Block Storage service. +package volumetypes diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/results.go new file mode 100644 index 000000000000..39c8d6f7facc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes/results.go @@ -0,0 +1,37 @@ +package volumetypes + +import ( + "github.com/mitchellh/mapstructure" + os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes" + "github.com/rackspace/gophercloud/pagination" +) + +type VolumeType os.VolumeType + +type GetResult struct { + os.GetResult +} + +// Extract will get the Volume Type struct out of the response. +func (r GetResult) Extract() (*VolumeType, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.VolumeType, err +} + +func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) { + var response struct { + VolumeTypes []VolumeType `mapstructure:"volume_types"` + } + + err := mapstructure.Decode(page.(os.ListResult).Body, &response) + return response.VolumeTypes, err +} 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 000000000000..5af7e077844f --- /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 000000000000..731fc6dd00c1 --- /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 000000000000..5582306a8e50 --- /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 000000000000..7152fa23af38 --- /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 000000000000..d6d299df8ed9 --- /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 000000000000..4ad966eac8d3 --- /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 000000000000..07c93a8dcda7 --- /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 000000000000..328e1682d339 --- /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 000000000000..46b3d50a81f9 --- /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 000000000000..e3f145997735 --- /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 000000000000..6c48365e4edd --- /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 000000000000..ee6e2a54fc98 --- /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 new file mode 100644 index 000000000000..db3f305b52a3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client.go @@ -0,0 +1,214 @@ +package rackspace + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/utils" + tokens2 "github.com/rackspace/gophercloud/rackspace/identity/v2/tokens" +) + +const ( + // RackspaceUSIdentity is an identity endpoint located in the United States. + RackspaceUSIdentity = "https://identity.api.rackspacecloud.com/v2.0/" + + // RackspaceUKIdentity is an identity endpoint located in the UK. + RackspaceUKIdentity = "https://lon.identity.api.rackspacecloud.com/v2.0/" +) + +const ( + v20 = "v2.0" +) + +// NewClient creates a client that's prepared to communicate with the Rackspace API, but is not +// yet authenticated. Most users will probably prefer using the AuthenticatedClient function +// instead. +// +// Provide the base URL of the identity endpoint you wish to authenticate against as "endpoint". +// Often, this will be either RackspaceUSIdentity or RackspaceUKIdentity. +func NewClient(endpoint string) (*gophercloud.ProviderClient, error) { + if endpoint == "" { + return os.NewClient(RackspaceUSIdentity) + } + return os.NewClient(endpoint) +} + +// AuthenticatedClient logs in to Rackspace with the provided credentials and constructs a +// ProviderClient that's ready to operate. +// +// If the provided AuthOptions does not specify an explicit IdentityEndpoint, it will default to +// the canonical, production Rackspace US identity endpoint. +func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + client, err := NewClient(options.IdentityEndpoint) + if err != nil { + return nil, err + } + + err = Authenticate(client, options) + if err != nil { + return nil, err + } + return client, nil +} + +// Authenticate or re-authenticate against the most recent identity service supported at the +// provided endpoint. +func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + versions := []*utils.Version{ + &utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v20: + return v2auth(client, endpoint, options) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("Unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV2 explicitly authenticates with v2 of the identity service. +func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + return v2auth(client, "", options) +} + +func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error { + v2Client := NewIdentityV2(client) + if endpoint != "" { + v2Client.Endpoint = endpoint + } + + result := tokens2.Create(v2Client, tokens2.WrapOptions(options)) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + if options.AllowReauth { + client.ReauthFunc = func() error { + return AuthenticateV2(client, options) + } + } + client.TokenID = token.ID + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return os.V2EndpointURL(catalog, opts) + } + + return nil +} + +// NewIdentityV2 creates a ServiceClient that may be used to access the v2 identity service. +func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient { + v2Endpoint := client.IdentityBase + "v2.0/" + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: v2Endpoint, + } +} + +// NewComputeV2 creates a ServiceClient that may be used to access the v2 compute service. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("compute") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: url, + }, nil +} + +// NewObjectCDNV1 creates a ServiceClient that may be used with the Rackspace v1 CDN. +func NewObjectCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("rax:object-cdn") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the Rackspace v1 object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return os.NewObjectStorageV1(client, eo) +} + +// NewBlockStorageV1 creates a ServiceClient that can be used to access the +// Rackspace Cloud Block Storage v1 API. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("volume") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + + 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 +} + +// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service. +func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("orchestration") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewRackConnectV3 creates a ServiceClient that may be used to access the v3 RackConnect service. +func NewRackConnectV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("rax:rackconnect") + 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/client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client_test.go new file mode 100644 index 000000000000..73b1c886ffa8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/client_test.go @@ -0,0 +1,38 @@ +package rackspace + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestAuthenticatedClientV2(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "access": { + "token": { + "id": "01234567890", + "expires": "2014-10-01T10:00:00.000000Z" + }, + "serviceCatalog": [] + } + } + `) + }) + + options := gophercloud.AuthOptions{ + Username: "me", + APIKey: "09876543210", + IdentityEndpoint: th.Endpoint() + "v2.0/", + } + client, err := AuthenticatedClient(options) + th.AssertNoErr(t, err) + th.CheckEquals(t, "01234567890", client.TokenID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate.go new file mode 100644 index 000000000000..2580459f077e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate.go @@ -0,0 +1,12 @@ +package bootfromvolume + +import ( + "github.com/rackspace/gophercloud" + osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + osServers "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// Create requests the creation of a server from the given block device mapping. +func Create(client *gophercloud.ServiceClient, opts osServers.CreateOptsBuilder) osServers.CreateResult { + return osBFV.Create(client, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go new file mode 100644 index 000000000000..0b5352751b4c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume/delegate_test.go @@ -0,0 +1,52 @@ +package bootfromvolume + +import ( + "testing" + + osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + base := servers.CreateOpts{ + Name: "createdserver", + ImageRef: "asdfasdfasdf", + FlavorRef: "performance1-1", + } + + ext := osBFV.CreateOptsExt{ + CreateOptsBuilder: base, + BlockDevice: []osBFV.BlockDevice{ + osBFV.BlockDevice{ + UUID: "123456", + SourceType: osBFV.Image, + DestinationType: "volume", + VolumeSize: 10, + }, + }, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "flavorRef": "performance1-1", + "block_device_mapping_v2":[ + { + "uuid":"123456", + "source_type":"image", + "destination_type":"volume", + "boot_index": "0", + "delete_on_termination": "false", + "volume_size": "10" + } + ] + } + } + ` + actual, err := ext.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go new file mode 100644 index 000000000000..6bfc20c5644f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate.go @@ -0,0 +1,46 @@ +package flavors + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts helps control the results returned by the List() function. For example, a flavor with a +// minDisk field of 10 will not be returned if you specify MinDisk set to 20. +type ListOpts struct { + + // MinDisk and MinRAM, if provided, elide flavors that do not meet your criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // Marker specifies the ID of the last flavor in the previous page. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of flavors. + Limit int `q:"limit"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// ListDetail enumerates the server images available to your account. +func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.ListDetail(client, opts) +} + +// Get returns details about a single flavor, identity by ID. +func Get(client *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(client, id) +} + +// ExtractFlavors interprets a page of List results as Flavors. +func ExtractFlavors(page pagination.Page) ([]os.Flavor, error) { + return os.ExtractFlavors(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate_test.go new file mode 100644 index 000000000000..204081dd1794 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/delegate_test.go @@ -0,0 +1,62 @@ +package flavors + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListFlavors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ListOutput) + case "performance1-2": + fmt.Fprintf(w, `{ "flavors": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + count := 0 + err := ListDetail(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + actual, err := ExtractFlavors(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedFlavorSlice, actual) + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/performance1-1", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) + + actual, err := Get(client.ServiceClient(), "performance1-1").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &Performance1Flavor, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/doc.go new file mode 100644 index 000000000000..278229ab97b5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/doc.go @@ -0,0 +1,3 @@ +// Package flavors provides information and interaction with the flavor +// API resource for the Rackspace Cloud Servers service. +package flavors 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 new file mode 100644 index 000000000000..894f916f6759 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/flavors/fixtures.go @@ -0,0 +1,129 @@ +// +build fixtures + +package flavors + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" +) + +// ListOutput is a sample response of a flavor List request. +const ListOutput = ` +{ + "flavors": [ + { + "OS-FLV-EXT-DATA:ephemeral": 0, + "OS-FLV-WITH-EXT-SPECS:extra_specs": { + "class": "performance1", + "disk_io_index": "40", + "number_of_data_disks": "0", + "policy_class": "performance_flavor", + "resize_policy_class": "performance_flavor" + }, + "disk": 20, + "id": "performance1-1", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1", + "rel": "bookmark" + } + ], + "name": "1 GB Performance", + "ram": 1024, + "rxtx_factor": 200, + "swap": "", + "vcpus": 1 + }, + { + "OS-FLV-EXT-DATA:ephemeral": 20, + "OS-FLV-WITH-EXT-SPECS:extra_specs": { + "class": "performance1", + "disk_io_index": "40", + "number_of_data_disks": "1", + "policy_class": "performance_flavor", + "resize_policy_class": "performance_flavor" + }, + "disk": 40, + "id": "performance1-2", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-2", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-2", + "rel": "bookmark" + } + ], + "name": "2 GB Performance", + "ram": 2048, + "rxtx_factor": 400, + "swap": "", + "vcpus": 2 + } + ] +}` + +// GetOutput is a sample response from a flavor Get request. Its contents correspond to the +// Performance1Flavor struct. +const GetOutput = ` +{ + "flavor": { + "OS-FLV-EXT-DATA:ephemeral": 0, + "OS-FLV-WITH-EXT-SPECS:extra_specs": { + "class": "performance1", + "disk_io_index": "40", + "number_of_data_disks": "0", + "policy_class": "performance_flavor", + "resize_policy_class": "performance_flavor" + }, + "disk": 20, + "id": "performance1-1", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1", + "rel": "bookmark" + } + ], + "name": "1 GB Performance", + "ram": 1024, + "rxtx_factor": 200, + "swap": "", + "vcpus": 1 + } +} +` + +// Performance1Flavor is the expected result of parsing GetOutput, or the first element of +// ListOutput. +var Performance1Flavor = os.Flavor{ + ID: "performance1-1", + Disk: 20, + RAM: 1024, + Name: "1 GB Performance", + RxTxFactor: 200.0, + Swap: 0, + VCPUs: 1, +} + +// Performance2Flavor is the second result expected from parsing ListOutput. +var Performance2Flavor = os.Flavor{ + ID: "performance1-2", + Disk: 40, + RAM: 2048, + Name: "2 GB Performance", + RxTxFactor: 400.0, + Swap: 0, + VCPUs: 2, +} + +// ExpectedFlavorSlice is the slice of Flavor structs that are expected to be parsed from +// ListOutput. +var ExpectedFlavorSlice = []os.Flavor{Performance1Flavor, Performance2Flavor} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate.go new file mode 100644 index 000000000000..18e1f315af94 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate.go @@ -0,0 +1,22 @@ +package images + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/images" + "github.com/rackspace/gophercloud/pagination" +) + +// ListDetail enumerates the available server images. +func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.ListDetail(client, opts) +} + +// Get acquires additional detail about a specific image by ID. +func Get(client *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(client, id) +} + +// ExtractImages interprets a page as a collection of server images. +func ExtractImages(page pagination.Page) ([]os.Image, error) { + return os.ExtractImages(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate_test.go new file mode 100644 index 000000000000..db0a6e3414c7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/delegate_test.go @@ -0,0 +1,62 @@ +package images + +import ( + "fmt" + "net/http" + "testing" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListImageDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ListOutput) + case "e19a734c-c7e6-443a-830c-242209c4d65d": + fmt.Fprintf(w, `{ "images": [] }`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) + + count := 0 + err := ListDetail(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractImages(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedImageSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetImageDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/images/e19a734c-c7e6-443a-830c-242209c4d65d", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) + + actual, err := Get(client.ServiceClient(), "e19a734c-c7e6-443a-830c-242209c4d65d").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &UbuntuImage, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/doc.go new file mode 100644 index 000000000000..cfae80671278 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/doc.go @@ -0,0 +1,3 @@ +// Package images provides information and interaction with the image +// API resource for the Rackspace Cloud Servers service. +package images 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 new file mode 100644 index 000000000000..ccfbdc6a1e44 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/images/fixtures.go @@ -0,0 +1,200 @@ +// +build fixtures + +package images + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/images" +) + +// ListOutput is an example response from an /images/detail request. +const ListOutput = ` +{ + "images": [ + { + "OS-DCF:diskConfig": "MANUAL", + "OS-EXT-IMG-SIZE:size": 1.017415075e+09, + "created": "2014-10-01T15:49:02Z", + "id": "30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "rel": "bookmark" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "auto_disk_config": "disabled", + "cache_in_nova": "True", + "com.rackspace__1__build_core": "1", + "com.rackspace__1__build_managed": "1", + "com.rackspace__1__build_rackconnect": "1", + "com.rackspace__1__options": "0", + "com.rackspace__1__platform_target": "PublicCloud", + "com.rackspace__1__release_build_date": "2014-10-01_15-46-08", + "com.rackspace__1__release_id": "100", + "com.rackspace__1__release_version": "10", + "com.rackspace__1__source": "kickstart", + "com.rackspace__1__visible_core": "1", + "com.rackspace__1__visible_managed": "0", + "com.rackspace__1__visible_rackconnect": "0", + "image_type": "base", + "org.openstack__1__architecture": "x64", + "org.openstack__1__os_distro": "org.archlinux", + "org.openstack__1__os_version": "2014.8", + "os_distro": "arch", + "os_type": "linux", + "vm_mode": "hvm" + }, + "minDisk": 20, + "minRam": 512, + "name": "Arch 2014.10 (PVHVM)", + "progress": 100, + "status": "ACTIVE", + "updated": "2014-10-01T19:37:58Z" + }, + { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-IMG-SIZE:size": 1.060306463e+09, + "created": "2014-10-01T12:58:11Z", + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "auto_disk_config": "True", + "cache_in_nova": "True", + "com.rackspace__1__build_core": "1", + "com.rackspace__1__build_managed": "1", + "com.rackspace__1__build_rackconnect": "1", + "com.rackspace__1__options": "0", + "com.rackspace__1__platform_target": "PublicCloud", + "com.rackspace__1__release_build_date": "2014-10-01_12-31-03", + "com.rackspace__1__release_id": "1007", + "com.rackspace__1__release_version": "6", + "com.rackspace__1__source": "kickstart", + "com.rackspace__1__visible_core": "1", + "com.rackspace__1__visible_managed": "1", + "com.rackspace__1__visible_rackconnect": "1", + "image_type": "base", + "org.openstack__1__architecture": "x64", + "org.openstack__1__os_distro": "com.ubuntu", + "org.openstack__1__os_version": "14.04", + "os_distro": "ubuntu", + "os_type": "linux", + "vm_mode": "xen" + }, + "minDisk": 20, + "minRam": 512, + "name": "Ubuntu 14.04 LTS (Trusty Tahr)", + "progress": 100, + "status": "ACTIVE", + "updated": "2014-10-01T15:51:44Z" + } + ] +} +` + +// GetOutput is an example response from an /images request. +const GetOutput = ` +{ + "image": { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-IMG-SIZE:size": 1060306463, + "created": "2014-10-01T12:58:11Z", + "id": "e19a734c-c7e6-443a-830c-242209c4d65d", + "links": [ + { + "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "self" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "bookmark" + }, + { + "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d", + "rel": "alternate", + "type": "application/vnd.openstack.image" + } + ], + "metadata": { + "auto_disk_config": "True", + "cache_in_nova": "True", + "com.rackspace__1__build_core": "1", + "com.rackspace__1__build_managed": "1", + "com.rackspace__1__build_rackconnect": "1", + "com.rackspace__1__options": "0", + "com.rackspace__1__platform_target": "PublicCloud", + "com.rackspace__1__release_build_date": "2014-10-01_12-31-03", + "com.rackspace__1__release_id": "1007", + "com.rackspace__1__release_version": "6", + "com.rackspace__1__source": "kickstart", + "com.rackspace__1__visible_core": "1", + "com.rackspace__1__visible_managed": "1", + "com.rackspace__1__visible_rackconnect": "1", + "image_type": "base", + "org.openstack__1__architecture": "x64", + "org.openstack__1__os_distro": "com.ubuntu", + "org.openstack__1__os_version": "14.04", + "os_distro": "ubuntu", + "os_type": "linux", + "vm_mode": "xen" + }, + "minDisk": 20, + "minRam": 512, + "name": "Ubuntu 14.04 LTS (Trusty Tahr)", + "progress": 100, + "status": "ACTIVE", + "updated": "2014-10-01T15:51:44Z" + } +} +` + +// ArchImage is the first Image structure that should be parsed from ListOutput. +var ArchImage = os.Image{ + ID: "30aa010e-080e-4d4b-a7f9-09fc55b07d69", + Name: "Arch 2014.10 (PVHVM)", + Created: "2014-10-01T15:49:02Z", + Updated: "2014-10-01T19:37:58Z", + MinDisk: 20, + MinRAM: 512, + Progress: 100, + Status: "ACTIVE", +} + +// UbuntuImage is the second Image structure that should be parsed from ListOutput and +// the only image that should be extracted from GetOutput. +var UbuntuImage = os.Image{ + ID: "e19a734c-c7e6-443a-830c-242209c4d65d", + Name: "Ubuntu 14.04 LTS (Trusty Tahr)", + Created: "2014-10-01T12:58:11Z", + Updated: "2014-10-01T15:51:44Z", + MinDisk: 20, + MinRAM: 512, + Progress: 100, + Status: "ACTIVE", +} + +// ExpectedImageSlice is the collection of images that should be parsed from ListOutput, +// in order. +var ExpectedImageSlice = []os.Image{ArchImage, UbuntuImage} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate.go new file mode 100644 index 000000000000..3e53525dc7c2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate.go @@ -0,0 +1,33 @@ +package keypairs + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return os.List(client) +} + +// Create requests the creation of a new keypair on the server, or to import a pre-existing +// keypair. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(client, opts) +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *gophercloud.ServiceClient, name string) os.GetResult { + return os.Get(client, name) +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *gophercloud.ServiceClient, name string) os.DeleteResult { + return os.Delete(client, name) +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(page pagination.Page) ([]os.KeyPair, error) { + return os.ExtractKeyPairs(page) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate_test.go new file mode 100644 index 000000000000..62e5df950ce1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/delegate_test.go @@ -0,0 +1,72 @@ +package keypairs + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "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() + os.HandleListSuccessfully(t) + + count := 0 + err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractKeyPairs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, os.ExpectedKeyPairSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateSuccessfully(t) + + actual, err := Create(client.ServiceClient(), os.CreateOpts{ + Name: "createdkey", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.CreatedKeyPair, actual) +} + +func TestImport(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleImportSuccessfully(t) + + actual, err := Create(client.ServiceClient(), os.CreateOpts{ + Name: "importedkey", + PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.ImportedKeyPair, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetSuccessfully(t) + + actual, err := Get(client.ServiceClient(), "firstkey").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.FirstKeyPair, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteSuccessfully(t) + + err := Delete(client.ServiceClient(), "deletedkey").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/doc.go new file mode 100644 index 000000000000..31713752eaea --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs/doc.go @@ -0,0 +1,3 @@ +// Package keypairs provides information and interaction with the keypair +// API resource for the Rackspace Cloud Servers service. +package keypairs diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/doc.go new file mode 100644 index 000000000000..8e5c77382d3e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/doc.go @@ -0,0 +1,3 @@ +// Package networks provides information and interaction with the network +// API resource for the Rackspace Cloud Servers service. +package networks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests.go new file mode 100644 index 000000000000..cebbffd36a60 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests.go @@ -0,0 +1,89 @@ +package networks + +import ( + "errors" + + "github.com/rackspace/gophercloud" + "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) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(c, listURL(c), createPage) +} + +// Get retrieves a specific network based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c, id), &res.Body, nil) + return res +} + +// 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 { + ToNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // REQUIRED. See Network object for more info. + CIDR string + // REQUIRED. See Network object for more info. + Label string +} + +// ToNetworkCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + n := make(map[string]interface{}) + + if opts.CIDR == "" { + return nil, errors.New("Required field CIDR not set.") + } + if opts.Label == "" { + return nil, errors.New("Required field Label not set.") + } + + n["label"] = opts.Label + n["cidr"] = opts.CIDR + return map[string]interface{}{"network": n}, nil +} + +// 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 CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToNetworkCreateMap() + if err != nil { + res.Err = err + return res + } + + // Send request to API + _, res.Err = c.Post(createURL(c), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + return res +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(deleteURL(c, networkID), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests_test.go new file mode 100644 index 000000000000..6f44c1caba75 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/requests_test.go @@ -0,0 +1,156 @@ +package networks + +import ( + "fmt" + "net/http" + "testing" + + "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() + + th.Mux.HandleFunc("/os-networksv2", 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": [ + { + "label": "test-network-1", + "cidr": "192.168.100.0/24", + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22" + }, + { + "label": "test-network-2", + "cidr": "192.30.250.00/18", + "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324" + } + ] +} + `) + }) + + client := fake.ServiceClient() + count := 0 + + err := List(client).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNetworks(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []Network{ + Network{ + Label: "test-network-1", + CIDR: "192.168.100.0/24", + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + }, + Network{ + Label: "test-network-2", + CIDR: "192.30.250.00/18", + ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324", + }, + } + + 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() + + th.Mux.HandleFunc("/os-networksv2/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": { + "label": "test-network-1", + "cidr": "192.168.100.0/24", + "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.CIDR, "192.168.100.0/24") + th.AssertEquals(t, n.Label, "test-network-1") + th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-networksv2", 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": { + "label": "test-network-1", + "cidr": "192.168.100.0/24" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "network": { + "label": "test-network-1", + "cidr": "192.168.100.0/24", + "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + } +} + `) + }) + + options := CreateOpts{Label: "test-network-1", CIDR: "192.168.100.0/24"} + n, err := Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, n.Label, "test-network-1") + th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c") +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/os-networksv2/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/compute/v2/networks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/results.go new file mode 100644 index 000000000000..eb6a76c008c6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/results.go @@ -0,0 +1,81 @@ +package networks + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*Network, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Network *Network `json:"network"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Network, 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 +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Network represents, well, a network. +type Network struct { + // UUID for the network + ID string `mapstructure:"id" json:"id"` + + // Human-readable name for the network. Might not be unique. + Label string `mapstructure:"label" json:"label"` + + // Classless Inter-Domain Routing + CIDR string `mapstructure:"cidr" json:"cidr"` +} + +// NetworkPage is the page returned by a pager when traversing over a +// collection of networks. +type NetworkPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if the NetworkPage contains no Networks. +func (r NetworkPage) IsEmpty() (bool, error) { + networks, err := ExtractNetworks(r) + if err != nil { + return true, err + } + return len(networks) == 0, nil +} + +// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct, +// and extracts the elements into a slice of Network structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractNetworks(page pagination.Page) ([]Network, error) { + var resp struct { + Networks []Network `mapstructure:"networks" json:"networks"` + } + + err := mapstructure.Decode(page.(NetworkPage).Body, &resp) + + return resp.Networks, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls.go new file mode 100644 index 000000000000..19a21aa90da9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls.go @@ -0,0 +1,27 @@ +package networks + +import "github.com/rackspace/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("os-networksv2", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-networksv2") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls_test.go new file mode 100644 index 000000000000..983992e2b9b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/networks/urls_test.go @@ -0,0 +1,38 @@ +package networks + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestGetURL(t *testing.T) { + actual := getURL(endpointClient(), "foo") + expected := endpoint + "os-networksv2/foo" + th.AssertEquals(t, expected, actual) +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "os-networksv2" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := createURL(endpointClient()) + expected := endpoint + "os-networksv2" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "foo") + expected := endpoint + "os-networksv2/foo" + th.AssertEquals(t, expected, actual) +} 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 new file mode 100644 index 000000000000..7810d156a0fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate.go @@ -0,0 +1,116 @@ +package servers + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" +) + +// List makes a request against the API to list servers accessible to you. +func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(client, opts) +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + 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) +} + +// Get requests details on a single server, by ID. +func Get(client *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(client, id) +} + +// ChangeAdminPassword alters the administrator or root password for a specified server. +func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) os.ActionResult { + return os.ChangeAdminPassword(client, id, newPassword) +} + +// Reboot requests that a given server reboot. Two methods exist for rebooting a server: +// +// os.HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the +// machine, or if a VM, terminating it at the hypervisor level. It's done. Caput. Full stop. Then, +// after a brief wait, power is restored or the VM instance restarted. +// +// os.SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures. E.g., in +// Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine. +func Reboot(client *gophercloud.ServiceClient, id string, how os.RebootMethod) os.ActionResult { + return os.Reboot(client, id, how) +} + +// Rebuild will reprovision the server according to the configuration options provided in the +// RebuildOpts struct. +func Rebuild(client *gophercloud.ServiceClient, id string, opts os.RebuildOptsBuilder) os.RebuildResult { + return os.Rebuild(client, id, opts) +} + +// Resize instructs the provider to change the flavor of the server. +// Note that this implies rebuilding it. +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in RESIZE_VERIFY state. +// While in this state, you can explore the use of the new server's configuration. +// If you like it, call ConfirmResize() to commit the resize permanently. +// Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *gophercloud.ServiceClient, id string, opts os.ResizeOptsBuilder) os.ActionResult { + return os.Resize(client, id, opts) +} + +// ConfirmResize confirms a previous resize operation on a server. +// See Resize() for more details. +func ConfirmResize(client *gophercloud.ServiceClient, id string) os.ActionResult { + return os.ConfirmResize(client, id) +} + +// WaitForStatus will continually poll a server until it successfully transitions to a specified +// status. It will do this for at most the number of seconds specified. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return os.WaitForStatus(c, id, status, secs) +} + +// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities. +func ExtractServers(page pagination.Page) ([]os.Server, error) { + return os.ExtractServers(page) +} + +// ListAddresses makes a request against the API to list the servers IP addresses. +func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager { + return os.ListAddresses(client, id) +} + +// ExtractAddresses interprets the results of a single page from a ListAddresses() call, producing a map of Address slices. +func ExtractAddresses(page pagination.Page) (map[string][]os.Address, error) { + return os.ExtractAddresses(page) +} + +// ListAddressesByNetwork makes a request against the API to list the servers IP addresses +// for the given network. +func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager { + return os.ListAddressesByNetwork(client, id, network) +} + +// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call, producing a map of Address slices. +func ExtractNetworkAddresses(page pagination.Page) ([]os.Address, error) { + return os.ExtractNetworkAddresses(page) +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(client *gophercloud.ServiceClient, id string) os.GetMetadataResult { + return os.Metadata(client, id) +} + +// 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 os.UpdateMetadataOptsBuilder) os.UpdateMetadataResult { + return os.UpdateMetadata(client, id, opts) +} 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 new file mode 100644 index 000000000000..03e7acea846a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/delegate_test.go @@ -0,0 +1,182 @@ +package servers + +import ( + "fmt" + "net/http" + "testing" + + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListServers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ListOutput) + }) + + count := 0 + err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractServers(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedServerSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreateServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleServerCreationSuccessfully(t, CreateOutput) + + actual, err := Create(client.ServiceClient(), os.CreateOpts{ + Name: "derp", + ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb", + FlavorRef: "1", + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, &CreatedServer, actual) +} + +func TestDeleteServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleServerDeletionSuccessfully(t) + + res := Delete(client.ServiceClient(), "asdfasdfasdf") + th.AssertNoErr(t, res.Err) +} + +func TestGetServer(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, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) + + actual, err := Get(client.ServiceClient(), "8c65cb68-0681-4c30-bc88-6b83a8a26aee").Extract() + th.AssertNoErr(t, err) + 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() + os.HandleAdminPasswordChangeSuccessfully(t) + + res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password") + th.AssertNoErr(t, res.Err) +} + +func TestReboot(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleRebootSuccessfully(t) + + res := Reboot(client.ServiceClient(), "1234asdf", os.SoftReboot) + th.AssertNoErr(t, res.Err) +} + +func TestRebuildServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleRebuildSuccessfully(t, GetOutput) + + opts := os.RebuildOpts{ + Name: "new-name", + AdminPass: "swordfish", + ImageID: "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + AccessIPv4: "1.2.3.4", + } + actual, err := Rebuild(client.ServiceClient(), "1234asdf", opts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &GophercloudServer, actual) +} + +func TestListAddresses(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleAddressListSuccessfully(t) + + expected := os.ListAddressesExpected + pages := 0 + err := ListAddresses(client.ServiceClient(), "asdfasdfasdf").EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := ExtractAddresses(page) + th.AssertNoErr(t, err) + + if len(actual) != 2 { + t.Fatalf("Expected 2 networks, got %d", len(actual)) + } + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, pages) +} + +func TestListAddressesByNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleNetworkAddressListSuccessfully(t) + + expected := os.ListNetworkAddressesExpected + pages := 0 + err := ListAddressesByNetwork(client.ServiceClient(), "asdfasdfasdf", "public").EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := ExtractNetworkAddresses(page) + th.AssertNoErr(t, err) + + if len(actual) != 2 { + t.Fatalf("Expected 2 addresses, got %d", len(actual)) + } + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, pages) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/doc.go new file mode 100644 index 000000000000..c9f77f6945df --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/doc.go @@ -0,0 +1,3 @@ +// Package servers provides information and interaction with the server +// API resource for the Rackspace Cloud Servers service. +package servers 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 new file mode 100644 index 000000000000..75cccd04181f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/fixtures.go @@ -0,0 +1,574 @@ +// +build fixtures + +package servers + +import ( + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// ListOutput is the recorded output of a Rackspace servers.List request. +const ListOutput = ` +{ + "servers": [ + { + "OS-DCF:diskConfig": "MANUAL", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "accessIPv4": "1.2.3.4", + "accessIPv6": "1111:4822:7818:121:2000:9b5e:7438:a2d0", + "addresses": { + "private": [ + { + "addr": "10.208.230.113", + "version": 4 + } + ], + "public": [ + { + "addr": "2001:4800:7818:101:2000:9b5e:7428:a2d0", + "version": 6 + }, + { + "addr": "104.130.131.164", + "version": 4 + } + ] + }, + "created": "2014-09-23T12:34:58Z", + "flavor": { + "id": "performance1-8", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8", + "rel": "bookmark" + } + ] + }, + "hostId": "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475", + "id": "59818cee-bc8c-44eb-8073-673ee65105f7", + "image": { + "id": "255df5fb-e3d4-45a3-9a07-c976debf7c14", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14", + "rel": "bookmark" + } + ] + }, + "key_name": "mykey", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "devstack", + "progress": 100, + "status": "ACTIVE", + "tenant_id": "111111", + "updated": "2014-09-23T12:38:19Z", + "user_id": "14ae7bb21d81422694655f3cc30f2930" + }, + { + "OS-DCF:diskConfig": "MANUAL", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "accessIPv4": "1.1.2.3", + "accessIPv6": "2222:4444:7817:101:be76:4eff:f0e5:9e02", + "addresses": { + "private": [ + { + "addr": "10.10.20.30", + "version": 4 + } + ], + "public": [ + { + "addr": "1.1.2.3", + "version": 4 + }, + { + "addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02", + "version": 6 + } + ] + }, + "created": "2014-07-21T19:32:55Z", + "flavor": { + "id": "performance1-2", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2", + "rel": "bookmark" + } + ] + }, + "hostId": "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c", + "id": "25f1c7f5-e00a-4715-b354-16e24b2f4630", + "image": { + "id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "rel": "bookmark" + } + ] + }, + "key_name": "otherkey", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "bookmark" + } + ], + "metadata": {}, + "name": "peril-dfw", + "progress": 100, + "status": "ACTIVE", + "tenant_id": "111111", + "updated": "2014-07-21T19:34:24Z", + "user_id": "14ae7bb21d81422694655f3cc30f2930" + } + ] +} +` + +// GetOutput is the recorded output of a Rackspace servers.Get request. +const GetOutput = ` +{ + "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": "Gophercloud-pxpGGuey", + "progress": 100, + "status": "ACTIVE", + "tenant_id": "111111", + "updated": "2014-10-21T14:42:57Z", + "user_id": "14ae7bb21d81423694655f4dd30f2930" + } +} +` + +// 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 = ` +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "adminPass": "v7tADqbE5pr9", + "id": "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + "links": [ + { + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + "rel": "self" + }, + { + "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e", + "rel": "bookmark" + } + ] + } +} +` + +// DevstackServer is the expected first result from parsing ListOutput. +var DevstackServer = os.Server{ + ID: "59818cee-bc8c-44eb-8073-673ee65105f7", + Name: "devstack", + TenantID: "111111", + UserID: "14ae7bb21d81422694655f3cc30f2930", + HostID: "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475", + Updated: "2014-09-23T12:38:19Z", + Created: "2014-09-23T12:34:58Z", + AccessIPv4: "1.2.3.4", + AccessIPv6: "1111:4822:7818:121:2000:9b5e:7438:a2d0", + Progress: 100, + Status: "ACTIVE", + Image: map[string]interface{}{ + "id": "255df5fb-e3d4-45a3-9a07-c976debf7c14", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "performance1-8", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "addr": "10.20.30.40", + "version": float64(4.0), + }, + }, + "public": []interface{}{ + map[string]interface{}{ + "addr": "1111:4822:7818:121:2000:9b5e:7438:a2d0", + "version": float64(6.0), + }, + map[string]interface{}{ + "addr": "1.2.3.4", + "version": float64(4.0), + }, + }, + }, + Metadata: map[string]interface{}{}, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59918cee-bd9d-44eb-8173-673ee75105f7", + "rel": "self", + }, + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7", + "rel": "bookmark", + }, + }, + KeyName: "mykey", + AdminPass: "", +} + +// PerilServer is the expected second result from parsing ListOutput. +var PerilServer = os.Server{ + ID: "25f1c7f5-e00a-4715-b354-16e24b2f4630", + Name: "peril-dfw", + TenantID: "111111", + UserID: "14ae7bb21d81422694655f3cc30f2930", + HostID: "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c", + Updated: "2014-07-21T19:34:24Z", + Created: "2014-07-21T19:32:55Z", + AccessIPv4: "1.1.2.3", + AccessIPv6: "2222:4444:7817:101:be76:4eff:f0e5:9e02", + Progress: 100, + Status: "ACTIVE", + Image: map[string]interface{}{ + "id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca", + "rel": "bookmark", + }, + }, + }, + Flavor: map[string]interface{}{ + "id": "performance1-2", + "links": []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2", + "rel": "bookmark", + }, + }, + }, + Addresses: map[string]interface{}{ + "private": []interface{}{ + map[string]interface{}{ + "addr": "10.10.20.30", + "version": float64(4.0), + }, + }, + "public": []interface{}{ + map[string]interface{}{ + "addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02", + "version": float64(6.0), + }, + map[string]interface{}{ + "addr": "1.1.2.3", + "version": float64(4.0), + }, + }, + }, + Metadata: map[string]interface{}{}, + Links: []interface{}{ + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "self", + }, + map[string]interface{}{ + "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630", + "rel": "bookmark", + }, + }, + KeyName: "otherkey", + AdminPass: "", +} + +// GophercloudServer is the expected result from parsing GetOutput. +var GophercloudServer = os.Server{ + ID: "8c65cb68-0681-4c30-bc88-6b83a8a26aee", + Name: "Gophercloud-pxpGGuey", + 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: "", +} + +// 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", + AdminPass: "v7tADqbE5pr9", + Links: []interface{}{}, +} + +// ExpectedServerSlice is the collection of servers, in order, that should be parsed from ListOutput. +var ExpectedServerSlice = []os.Server{DevstackServer, PerilServer} 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 new file mode 100644 index 000000000000..809183ec7c03 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests.go @@ -0,0 +1,163 @@ +package servers + +import ( + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + os "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// CreateOpts specifies all of the options that Rackspace accepts in its Create request, including +// the union of all extensions that Rackspace supports. +type CreateOpts struct { + // Name [required] is the name to assign to the newly launched server. + Name string + + // ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state. + // Optional if using the boot-from-volume extension. + ImageRef string + + // FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs. + FlavorRef string + + // SecurityGroups [optional] lists the names of the security groups to which this server should belong. + SecurityGroups []string + + // UserData [optional] contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you. + UserData []byte + + // AvailabilityZone [optional] in which to launch the server. + AvailabilityZone string + + // Networks [optional] dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the tenant. + Networks []os.Network + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string + + // Personality [optional] includes the path and contents of a file to inject into the server at launch. + // The maximum size of the file is 255 bytes (decoded). + Personality []byte + + // ConfigDrive [optional] enables metadata injection through a configuration drive. + ConfigDrive bool + + // 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 + // server. See the "keypairs" extension in OpenStack compute v2. + KeyPair string + + // DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig" + // extension in OpenStack compute v2. + DiskConfig diskconfig.DiskConfig + + // BlockDevice [optional] will create the server from a volume, which is created from an image, + // a snapshot, or an another volume. + BlockDevice []bootfromvolume.BlockDevice +} + +// ToServerCreateMap constructs a request body using all of the OpenStack extensions that are +// active on Rackspace. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + base := os.CreateOpts{ + Name: opts.Name, + ImageRef: opts.ImageRef, + FlavorRef: opts.FlavorRef, + SecurityGroups: opts.SecurityGroups, + UserData: opts.UserData, + AvailabilityZone: opts.AvailabilityZone, + Networks: opts.Networks, + Metadata: opts.Metadata, + Personality: opts.Personality, + ConfigDrive: opts.ConfigDrive, + AdminPass: opts.AdminPass, + } + + drive := diskconfig.CreateOptsExt{ + CreateOptsBuilder: base, + DiskConfig: opts.DiskConfig, + } + + res, err := drive.ToServerCreateMap() + if err != nil { + return nil, err + } + + if len(opts.BlockDevice) != 0 { + bfv := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: drive, + BlockDevice: opts.BlockDevice, + } + + res, err = bfv.ToServerCreateMap() + if err != nil { + return nil, err + } + } + + // key_name doesn't actually come from the extension (or at least isn't documented there) so + // we need to add it manually. + serverMap := res["server"].(map[string]interface{}) + serverMap["key_name"] = opts.KeyPair + + return res, nil +} + +// RebuildOpts represents all of the configuration options used in a server rebuild operation that +// are supported by Rackspace. +type RebuildOpts struct { + // Required. The ID of the image you want your server to be provisioned on + ImageID string + + // Name to set the server to + Name string + + // Required. The server's admin password + AdminPass string + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string + + // Personality [optional] includes the path and contents of a file to inject into the server at launch. + // The maximum size of the file is 255 bytes (decoded). + Personality []byte + + // Rackspace-specific stuff begins here. + + // DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig" + // extension in OpenStack compute v2. + DiskConfig diskconfig.DiskConfig +} + +// ToServerRebuildMap constructs a request body using all of the OpenStack extensions that are +// active on Rackspace. +func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { + base := os.RebuildOpts{ + ImageID: opts.ImageID, + Name: opts.Name, + AdminPass: opts.AdminPass, + AccessIPv4: opts.AccessIPv4, + AccessIPv6: opts.AccessIPv6, + Metadata: opts.Metadata, + Personality: opts.Personality, + } + + drive := diskconfig.RebuildOptsExt{ + RebuildOptsBuilder: base, + DiskConfig: opts.DiskConfig, + } + + return drive.ToServerRebuildMap() +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go new file mode 100644 index 000000000000..3c0f806936f0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/servers/requests_test.go @@ -0,0 +1,57 @@ +package servers + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestCreateOpts(t *testing.T) { + opts := CreateOpts{ + Name: "createdserver", + ImageRef: "image-id", + FlavorRef: "flavor-id", + KeyPair: "mykey", + DiskConfig: diskconfig.Manual, + } + + expected := ` + { + "server": { + "name": "createdserver", + "imageRef": "image-id", + "flavorRef": "flavor-id", + "key_name": "mykey", + "OS-DCF:diskConfig": "MANUAL" + } + } + ` + actual, err := opts.ToServerCreateMap() + th.AssertNoErr(t, err) + th.CheckJSONEquals(t, expected, actual) +} + +func TestRebuildOpts(t *testing.T) { + opts := RebuildOpts{ + Name: "rebuiltserver", + AdminPass: "swordfish", + ImageID: "asdfasdfasdf", + DiskConfig: diskconfig.Auto, + } + + actual, err := opts.ToServerRebuildMap() + th.AssertNoErr(t, err) + + expected := ` + { + "rebuild": { + "name": "rebuiltserver", + "imageRef": "asdfasdfasdf", + "adminPass": "swordfish", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests.go new file mode 100644 index 000000000000..1ff7c5ae55f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests.go @@ -0,0 +1,45 @@ +package virtualinterfaces + +import ( + "github.com/rackspace/gophercloud" + "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, instanceID string) pagination.Pager { + createPage := func(r pagination.PageResult) pagination.Page { + return VirtualInterfacePage{pagination.SinglePageBase(r)} + } + + return pagination.NewPager(c, listURL(c, instanceID), createPage) +} + +// Create creates a new virtual interface for a network and attaches the network +// to the server instance. +func Create(c *gophercloud.ServiceClient, instanceID, networkID string) CreateResult { + var res CreateResult + + reqBody := map[string]map[string]string{ + "virtual_interface": { + "network_id": networkID, + }, + } + + // Send request to API + _, res.Err = c.Post(createURL(c, instanceID), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + return res +} + +// Delete deletes the interface with interfaceID attached to the instance with +// instanceID. +func Delete(c *gophercloud.ServiceClient, instanceID, interfaceID string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(deleteURL(c, instanceID, interfaceID), &gophercloud.RequestOpts{ + OkCodes: []int{200, 204}, + }) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests_test.go new file mode 100644 index 000000000000..d40af9c46256 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/requests_test.go @@ -0,0 +1,165 @@ +package virtualinterfaces + +import ( + "fmt" + "net/http" + "testing" + + "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() + + th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2", 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, ` +{ + "virtual_interfaces": [ + { + "id": "de7c6d53-b895-4b4a-963c-517ccb0f0775", + "ip_addresses": [ + { + "address": "192.168.0.2", + "network_id": "f212726e-6321-4210-9bae-a13f5a33f83f", + "network_label": "superprivate_xml" + } + ], + "mac_address": "BC:76:4E:04:85:20" + }, + { + "id": "e14e789d-3b98-44a6-9c2d-c23eb1d1465c", + "ip_addresses": [ + { + "address": "10.181.1.30", + "network_id": "3b324a1b-31b8-4db5-9fe5-4a2067f60297", + "network_label": "private" + } + ], + "mac_address": "BC:76:4E:04:81:55" + } + ] +} + `) + }) + + client := fake.ServiceClient() + count := 0 + + err := List(client, "12345").EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractVirtualInterfaces(page) + if err != nil { + t.Errorf("Failed to extract networks: %v", err) + return false, err + } + + expected := []VirtualInterface{ + VirtualInterface{ + MACAddress: "BC:76:4E:04:85:20", + IPAddresses: []IPAddress{ + IPAddress{ + Address: "192.168.0.2", + NetworkID: "f212726e-6321-4210-9bae-a13f5a33f83f", + NetworkLabel: "superprivate_xml", + }, + }, + ID: "de7c6d53-b895-4b4a-963c-517ccb0f0775", + }, + VirtualInterface{ + MACAddress: "BC:76:4E:04:81:55", + IPAddresses: []IPAddress{ + IPAddress{ + Address: "10.181.1.30", + NetworkID: "3b324a1b-31b8-4db5-9fe5-4a2067f60297", + NetworkLabel: "private", + }, + }, + ID: "e14e789d-3b98-44a6-9c2d-c23eb1d1465c", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2", 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, ` +{ + "virtual_interface": { + "network_id": "6789" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, `{ + "virtual_interfaces": [ + { + "id": "de7c6d53-b895-4b4a-963c-517ccb0f0775", + "ip_addresses": [ + { + "address": "192.168.0.2", + "network_id": "f212726e-6321-4210-9bae-a13f5a33f83f", + "network_label": "superprivate_xml" + } + ], + "mac_address": "BC:76:4E:04:85:20" + } + ] + }`) + }) + + expected := &VirtualInterface{ + MACAddress: "BC:76:4E:04:85:20", + IPAddresses: []IPAddress{ + IPAddress{ + Address: "192.168.0.2", + NetworkID: "f212726e-6321-4210-9bae-a13f5a33f83f", + NetworkLabel: "superprivate_xml", + }, + }, + ID: "de7c6d53-b895-4b4a-963c-517ccb0f0775", + } + + actual, err := Create(fake.ServiceClient(), "12345", "6789").Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expected, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2/6789", 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(), "12345", "6789") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/results.go new file mode 100644 index 000000000000..26fa7f31ce09 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/results.go @@ -0,0 +1,81 @@ +package virtualinterfaces + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*VirtualInterface, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + VirtualInterfaces []VirtualInterface `mapstructure:"virtual_interfaces" json:"virtual_interfaces"` + } + + err := mapstructure.Decode(r.Body, &res) + + return &res.VirtualInterfaces[0], 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 +} + +// IPAddress represents a vitual address attached to a VirtualInterface. +type IPAddress struct { + Address string `mapstructure:"address" json:"address"` + NetworkID string `mapstructure:"network_id" json:"network_id"` + NetworkLabel string `mapstructure:"network_label" json:"network_label"` +} + +// VirtualInterface represents a virtual interface. +type VirtualInterface struct { + // UUID for the virtual interface + ID string `mapstructure:"id" json:"id"` + + MACAddress string `mapstructure:"mac_address" json:"mac_address"` + + IPAddresses []IPAddress `mapstructure:"ip_addresses" json:"ip_addresses"` +} + +// VirtualInterfacePage is the page returned by a pager when traversing over a +// collection of virtual interfaces. +type VirtualInterfacePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if the NetworkPage contains no Networks. +func (r VirtualInterfacePage) IsEmpty() (bool, error) { + networks, err := ExtractVirtualInterfaces(r) + if err != nil { + return true, err + } + return len(networks) == 0, nil +} + +// ExtractVirtualInterfaces accepts a Page struct, specifically a VirtualInterfacePage struct, +// and extracts the elements into a slice of VirtualInterface structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractVirtualInterfaces(page pagination.Page) ([]VirtualInterface, error) { + var resp struct { + VirtualInterfaces []VirtualInterface `mapstructure:"virtual_interfaces" json:"virtual_interfaces"` + } + + err := mapstructure.Decode(page.(VirtualInterfacePage).Body, &resp) + + return resp.VirtualInterfaces, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls.go new file mode 100644 index 000000000000..9e5693e8490e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls.go @@ -0,0 +1,15 @@ +package virtualinterfaces + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient, instanceID string) string { + return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2") +} + +func createURL(c *gophercloud.ServiceClient, instanceID string) string { + return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2") +} + +func deleteURL(c *gophercloud.ServiceClient, instanceID, interfaceID string) string { + return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2", interfaceID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls_test.go new file mode 100644 index 000000000000..6732e4ed9f41 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces/urls_test.go @@ -0,0 +1,32 @@ +package virtualinterfaces + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestCreateURL(t *testing.T) { + actual := createURL(endpointClient(), "12345") + expected := endpoint + "servers/12345/os-virtual-interfacesv2" + th.AssertEquals(t, expected, actual) +} + +func TestListURL(t *testing.T) { + actual := createURL(endpointClient(), "12345") + expected := endpoint + "servers/12345/os-virtual-interfacesv2" + th.AssertEquals(t, expected, actual) +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient(), "12345", "6789") + expected := endpoint + "servers/12345/os-virtual-interfacesv2/6789" + th.AssertEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/delegate.go new file mode 100644 index 000000000000..c6003e0e5b94 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/delegate.go @@ -0,0 +1,27 @@ +package volumeattach + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of VolumeAttachments. +func List(client *gophercloud.ServiceClient, serverID string) pagination.Pager { + return os.List(client, serverID) +} + +// Create requests the creation of a new volume attachment on the server +func Create(client *gophercloud.ServiceClient, serverID string, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(client, serverID, opts) +} + +// Get returns public data about a previously created VolumeAttachment. +func Get(client *gophercloud.ServiceClient, serverID, aID string) os.GetResult { + return os.Get(client, serverID, aID) +} + +// Delete requests the deletion of a previous stored VolumeAttachment from the server. +func Delete(client *gophercloud.ServiceClient, serverID, aID string) os.DeleteResult { + return os.Delete(client, serverID, aID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/delegate_test.go new file mode 100644 index 000000000000..e26416cba0de --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/delegate_test.go @@ -0,0 +1,66 @@ +package volumeattach + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" + "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() + os.HandleListSuccessfully(t) + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + count := 0 + err := List(client.ServiceClient(), serverId).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractVolumeAttachments(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, os.ExpectedVolumeAttachmentSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateSuccessfully(t) + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + actual, err := Create(client.ServiceClient(), serverId, os.CreateOpts{ + Device: "/dev/vdc", + VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804", + }).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.CreatedVolumeAttachment, actual) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetSuccessfully(t) + aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + actual, err := Get(client.ServiceClient(), serverId, aId).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &os.SecondVolumeAttachment, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteSuccessfully(t) + aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804" + serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0" + + err := Delete(client.ServiceClient(), serverId, aId).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/doc.go new file mode 100644 index 000000000000..2164908e3a89 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach/doc.go @@ -0,0 +1,3 @@ +// Package volumeattach provides the ability to attach and detach volume +// to instances to Rackspace servers +package volumeattach diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate.go new file mode 100644 index 000000000000..fc547cde5f4c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate.go @@ -0,0 +1,24 @@ +package extensions + +import ( + "github.com/rackspace/gophercloud" + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the +// elements into a slice of os.Extension structs. +func ExtractExtensions(page pagination.Page) ([]common.Extension, error) { + return common.ExtractExtensions(page) +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) common.GetResult { + return common.Get(c, alias) +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate_test.go new file mode 100644 index 000000000000..e30f79404dc1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/delegate_test.go @@ -0,0 +1,39 @@ +package extensions + +import ( + "testing" + + common "github.com/rackspace/gophercloud/openstack/common/extensions" + "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() + common.HandleListExtensionsSuccessfully(t) + + count := 0 + + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractExtensions(page) + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, common.ExpectedExtensions, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + common.HandleGetExtensionSuccessfully(t) + + actual, err := Get(fake.ServiceClient(), "agent").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, common.SingleExtension, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/doc.go new file mode 100644 index 000000000000..b02a95b534c9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the all the +// extensions available for the Rackspace Identity service. +package extensions 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 000000000000..a6ee8515ceb8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/roles/delegate.go @@ -0,0 +1,50 @@ +package roles + +import ( + "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 = client.Request("PUT", userRoleURL(client, userID, roleID), gophercloud.RequestOpts{ + 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 = client.Request("DELETE", userRoleURL(client, userID, roleID), gophercloud.RequestOpts{ + 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 000000000000..fcee97d0bc39 --- /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 000000000000..5f22d0f6424a --- /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/tenants/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate.go new file mode 100644 index 000000000000..6cdd0cfbdcff --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate.go @@ -0,0 +1,17 @@ +package tenants + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractTenants interprets a page of List results as a more usable slice of Tenant structs. +func ExtractTenants(page pagination.Page) ([]os.Tenant, error) { + return os.ExtractTenants(page) +} + +// List enumerates the tenants to which the current token grants access. +func List(client *gophercloud.ServiceClient, opts *os.ListOpts) pagination.Pager { + return os.List(client, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate_test.go new file mode 100644 index 000000000000..eccbfe23eb6e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/delegate_test.go @@ -0,0 +1,28 @@ +package tenants + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListTenants(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListTenantsSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + actual, err := ExtractTenants(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, os.ExpectedTenantSlice, actual) + + count++ + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/doc.go new file mode 100644 index 000000000000..c1825c21dcf5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tenants/doc.go @@ -0,0 +1,3 @@ +// Package tenants provides information and interaction with the tenant +// API resource for the Rackspace Identity service. +package tenants diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate.go new file mode 100644 index 000000000000..4f9885af03ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate.go @@ -0,0 +1,60 @@ +package tokens + +import ( + "errors" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" +) + +var ( + // ErrPasswordProvided is returned if both a password and an API key are provided to Create. + ErrPasswordProvided = errors.New("Please provide either a password or an API key.") +) + +// AuthOptions wraps the OpenStack AuthOptions struct to be able to customize the request body +// when API key authentication is used. +type AuthOptions struct { + os.AuthOptions +} + +// WrapOptions embeds a root AuthOptions struct in a package-specific one. +func WrapOptions(original gophercloud.AuthOptions) AuthOptions { + return AuthOptions{AuthOptions: os.WrapOptions(original)} +} + +// ToTokenCreateMap serializes an AuthOptions into a request body. If an API key is provided, it +// will be used, otherwise +func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) { + if auth.APIKey == "" { + return auth.AuthOptions.ToTokenCreateMap() + } + + // Verify that other required attributes are present. + if auth.Username == "" { + return nil, os.ErrUsernameRequired + } + + authMap := make(map[string]interface{}) + + authMap["RAX-KSKEY:apiKeyCredentials"] = map[string]interface{}{ + "username": auth.Username, + "apiKey": auth.APIKey, + } + + if auth.TenantID != "" { + authMap["tenantId"] = auth.TenantID + } + if auth.TenantName != "" { + authMap["tenantName"] = auth.TenantName + } + + return map[string]interface{}{"auth": authMap}, nil +} + +// Create authenticates to Rackspace's identity service and attempts to acquire a Token. Rather +// than interact with this service directly, users should generally call +// rackspace.AuthenticatedClient(). +func Create(client *gophercloud.ServiceClient, auth AuthOptions) os.CreateResult { + return os.Create(client, auth) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate_test.go new file mode 100644 index 000000000000..6678ff4a7c23 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/delegate_test.go @@ -0,0 +1,36 @@ +package tokens + +import ( + "testing" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) os.CreateResult { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleTokenPost(t, requestJSON) + + return Create(client.ServiceClient(), WrapOptions(options)) +} + +func TestCreateTokenWithAPIKey(t *testing.T) { + options := gophercloud.AuthOptions{ + Username: "me", + APIKey: "1234567890abcdef", + } + + os.IsSuccessful(t, tokenPost(t, options, ` + { + "auth": { + "RAX-KSKEY:apiKeyCredentials": { + "username": "me", + "apiKey": "1234567890abcdef" + } + } + } + `)) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/doc.go new file mode 100644 index 000000000000..44043e5e13fe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/tokens/doc.go @@ -0,0 +1,3 @@ +// Package tokens provides information and interaction with the token +// API resource for the Rackspace Identity service. +package tokens 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 000000000000..6135bec101a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/identity/v2/users/delegate.go @@ -0,0 +1,142 @@ +package users + +import ( + "errors" + + "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 = client.Request("POST", os.ResourceURL(client, id), gophercloud.RequestOpts{ + JSONResponse: &result.Body, + JSONBody: opts.ToUserUpdateMap(), + 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 = client.Request("POST", resetAPIKeyURL(client, id), gophercloud.RequestOpts{ + JSONResponse: &result.Body, + 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 000000000000..62faf0c5becf --- /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 000000000000..973f39ea8c9a --- /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 000000000000..6936ecba84ee --- /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 000000000000..bc1aaefb0223 --- /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 000000000000..42325fe83d5a --- /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 000000000000..e3c941c81bad --- /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 000000000000..d4ce7c01f447 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/acl/requests.go @@ -0,0 +1,111 @@ +package acl + +import ( + "errors" + "fmt" + + "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 = client.Post(rootURL(client, loadBalancerID), reqBody, nil, nil) + 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 = c.Delete(url, nil) + 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 = c.Delete(resourceURL(c, lbID, itemID), nil) + 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 = c.Delete(rootURL(c, lbID), nil) + 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 000000000000..c4961a3dd8f7 --- /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 000000000000..9ea5ea2f4b7d --- /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 000000000000..e373fa1d809e --- /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 000000000000..05f0032859bb --- /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 000000000000..6325310dbbb3 --- /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 000000000000..46f5f02a43a6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/lbs/requests.go @@ -0,0 +1,497 @@ +package lbs + +import ( + "errors" + + "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/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 = c.Post(rootURL(c), reqBody, &res.Body, nil) + 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 = c.Get(resourceURL(c, id), &res.Body, &gophercloud.RequestOpts{ + 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 = c.Delete(url, nil) + return res +} + +// Delete removes a single load balancer. +func Delete(c *gophercloud.ServiceClient, id int) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + 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 = c.Put(resourceURL(c, id), reqBody, nil, nil) + 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 := client.Get(loggingURL(client, id), &body, nil) + 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 { + var res gophercloud.ErrResult + _, res.Err = client.Put(loggingURL(client, id), toConnLoggingMap(true), nil, nil) + return res +} + +// DisableLogging will disable connection logging for a specified load balancer. +func DisableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + var res gophercloud.ErrResult + _, res.Err = client.Put(loggingURL(client, id), toConnLoggingMap(false), nil, nil) + 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 = client.Get(errorPageURL(client, id), &res.Body, nil) + 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 = client.Put(errorPageURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + 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 = client.Delete(errorPageURL(client, id), &gophercloud.RequestOpts{ + 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 = client.Get(statsURL(client, id), &res.Body, nil) + 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 := client.Get(cacheURL(client, id), &body, nil) + 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 { + var res gophercloud.ErrResult + _, res.Err = client.Put(cacheURL(client, id), toCachingMap(true), nil, nil) + return res +} + +// DisableCaching will disable content-caching for the specified load balancer. +func DisableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult { + var res gophercloud.ErrResult + _, res.Err = client.Put(cacheURL(client, id), toCachingMap(false), nil, nil) + 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 000000000000..a8ec19e07c3e --- /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 000000000000..98f3962d77ec --- /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 000000000000..471a86b0a7e2 --- /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 000000000000..2c5be75ae42f --- /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 000000000000..a565abced54c --- /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 000000000000..d4ba27653ca1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/monitors/requests.go @@ -0,0 +1,160 @@ +package monitors + +import ( + "errors" + + "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 = c.Put(rootURL(c, id), reqBody, nil, nil) + 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 = c.Get(rootURL(c, id), &res.Body, nil) + 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 = c.Delete(rootURL(c, id), nil) + 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 000000000000..76a60db7f458 --- /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 000000000000..eec556f343c6 --- /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 000000000000..0a1e6df5f493 --- /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 000000000000..49c431894a7f --- /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 000000000000..7c85945cafe1 --- /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 000000000000..02af86b5c1e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/nodes/requests.go @@ -0,0 +1,256 @@ +package nodes + +import ( + "errors" + "fmt" + + "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 := client.Post(rootURL(client, loadBalancerID), reqBody, &res.Body, nil) + + if err != nil { + res.Err = err + return res + } + + pr, err := pagination.PageResultFrom(resp) + 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 = c.Delete(url, nil) + 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 = c.Get(resourceURL(c, lbID, nodeID), &res.Body, nil) + 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 = c.Put(resourceURL(c, lbID, nodeID), reqBody, nil, nil) + 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 = c.Delete(resourceURL(c, lbID, nodeID), nil) + 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 000000000000..003d347c073b --- /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 000000000000..916485f2fcba --- /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 000000000000..2cefee26449d --- /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 000000000000..dcec0a87e2b0 --- /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 000000000000..9596819d16d5 --- /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 000000000000..a93d766cd922 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/sessions/requests.go @@ -0,0 +1,63 @@ +package sessions + +import ( + "errors" + + "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 = c.Put(rootURL(c, lbID), reqBody, &res.Body, nil) + 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 = c.Get(rootURL(c, lbID), &res.Body, nil) + 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 = c.Delete(rootURL(c, lbID), nil) + 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 000000000000..f319e540bb30 --- /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 000000000000..fe90e722cbd8 --- /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 000000000000..c4a896d90573 --- /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 000000000000..6a2c174ae930 --- /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 000000000000..1d401001250b --- /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 000000000000..bb53ef896021 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/ssl/requests.go @@ -0,0 +1,247 @@ +package ssl + +import ( + "errors" + + "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 = c.Put(rootURL(c, lbID), reqBody, &res.Body, &gophercloud.RequestOpts{ + 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 = c.Get(rootURL(c, lbID), &res.Body, nil) + 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 = c.Delete(rootURL(c, lbID), &gophercloud.RequestOpts{ + 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 = c.Post(certURL(c, lbID), reqBody, &res.Body, &gophercloud.RequestOpts{ + 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 = c.Get(certResourceURL(c, lbID, certID), &res.Body, nil) + 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 = c.Put(certResourceURL(c, lbID, certID), reqBody, &res.Body, nil) + 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 = c.Delete(certResourceURL(c, lbID, certID), &gophercloud.RequestOpts{ + 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 000000000000..fb14c4a28d5f --- /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 000000000000..ead9fcd37eb5 --- /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 000000000000..aa814b358345 --- /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 000000000000..1ed605d3629b --- /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 000000000000..40223f60a672 --- /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 000000000000..0446b97a14f5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/throttle/requests.go @@ -0,0 +1,76 @@ +package throttle + +import ( + "errors" + + "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 = c.Put(rootURL(c, lbID), reqBody, &res.Body, nil) + 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 = c.Get(rootURL(c, lbID), &res.Body, nil) + 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 = c.Delete(rootURL(c, lbID), nil) + 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 000000000000..6e9703ffce33 --- /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 000000000000..db93c6f3f4ff --- /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 000000000000..b77f0ac1c79a --- /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 000000000000..5c3846d44d64 --- /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 000000000000..158759f7fa13 --- /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 000000000000..2bc924f293b8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/lb/v1/vips/requests.go @@ -0,0 +1,97 @@ +package vips + +import ( + "errors" + + "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 = c.Post(rootURL(c, lbID), reqBody, &res.Body, nil) + 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 = c.Delete(url, nil) + 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 = c.Delete(resourceURL(c, lbID, vipID), nil) + 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 000000000000..74ac4617381a --- /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 000000000000..678b2aff7975 --- /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 000000000000..28f063a0f78b --- /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 000000000000..129cd63aee9f --- /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 000000000000..dcb0855dba3c --- /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 000000000000..f51c732d4367 --- /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 000000000000..95728d18558c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/ports/delegate.go @@ -0,0 +1,43 @@ +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. +// +// NOTE: Currently the SecurityGroup option is not implemented to work with +// Rackspace. +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 000000000000..f53ff595a047 --- /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/security/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/doc.go new file mode 100644 index 000000000000..31f744ccd7a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/doc.go @@ -0,0 +1,32 @@ +// Package security contains functionality to work with security group and +// security group rules Neutron resources. +// +// Security groups and security group rules allows administrators and tenants +// the ability to specify the type of traffic and direction (ingress/egress) +// that is allowed to pass through a port. A security group is a container for +// security group rules. +// +// When a port is created in Networking it is associated with a security group. +// If a security group is not specified the port is associated with a 'default' +// security group. By default, this group drops all ingress traffic and allows +// all egress. Rules can be added to this group in order to change the behaviour. +// +// The basic characteristics of Neutron Security Groups are: +// +// For ingress traffic (to an instance) +// - Only traffic matched with security group rules are allowed. +// - When there is no rule defined, all traffic is dropped. +// +// For egress traffic (from an instance) +// - Only traffic matched with security group rules are allowed. +// - When there is no rule defined, all egress traffic are dropped. +// - When a new security group is created, rules to allow all egress traffic +// is automatically added. +// +// "default security group" is defined for each tenant. +// - For the default security group a rule which allows intercommunication +// among hosts associated with the default security group is defined by default. +// - As a result, all egress traffic and intercommunication in the default +// group are allowed and all ingress from outside of the default group is +// dropped by default (in the default security group). +package security diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/groups/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/groups/delegate.go new file mode 100644 index 000000000000..1e9a23a05a13 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/groups/delegate.go @@ -0,0 +1,30 @@ +package groups + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager which allows you to iterate over a collection of +// security groups. 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.ListOpts) pagination.Pager { + return os.List(c, opts) +} + +// Create is an operation which provisions a new security group with default +// security group rules for the IPv4 and IPv6 ether types. +func Create(c *gophercloud.ServiceClient, opts os.CreateOpts) os.CreateResult { + return os.Create(c, opts) +} + +// Get retrieves a particular security group based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(c, id) +} + +// Delete will permanently delete a particular security group based on its unique ID. +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/networking/v2/security/groups/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/groups/delegate_test.go new file mode 100644 index 000000000000..45cd3ba8d4c1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/groups/delegate_test.go @@ -0,0 +1,206 @@ +package groups + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + osGroups "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups" + osRules "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", 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_groups": [ + { + "description": "default", + "id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "name": "default", + "security_group_rules": [], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ] + } + `) + }) + + count := 0 + + List(fake.ServiceClient(), osGroups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := osGroups.ExtractGroups(page) + if err != nil { + t.Errorf("Failed to extract secgroups: %v", err) + return false, err + } + + expected := []osGroups.SecGroup{ + osGroups.SecGroup{ + Description: "default", + ID: "85cc3048-abc3-43cc-89b3-377341426ac5", + Name: "default", + Rules: []osRules.SecGroupRule{}, + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", 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, ` + { + "security_group": { + "name": "new-webservers", + "description": "security group for webservers" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` + { + "security_group": { + "description": "security group for webservers", + "id": "2076db17-a522-4506-91de-c6dd8e837028", + "name": "new-webservers", + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv4", + "id": "38ce2d8e-e8f1-48bd-83c2-d33cb9f50c3d", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv6", + "id": "565b9502-12de-4ffd-91e9-68885cff6ae1", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + } + `) + }) + + opts := osGroups.CreateOpts{Name: "new-webservers", Description: "security group for webservers"} + _, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/85cc3048-abc3-43cc-89b3-377341426ac5", 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": "85cc3048-abc3-43cc-89b3-377341426ac5", + "name": "default", + "security_group_rules": [ + { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv4", + "id": "93aa42e5-80db-4581-9391-3a608bd0e448", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ], + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + } + `) + }) + + sg, err := Get(fake.ServiceClient(), "85cc3048-abc3-43cc-89b3-377341426ac5").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "default", sg.Description) + th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sg.ID) + th.AssertEquals(t, "default", sg.Name) + th.AssertEquals(t, 2, len(sg.Rules)) + th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sg.TenantID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/4ec89087-d057-4e2c-911f-60a3b47ee304", 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(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + th.AssertNoErr(t, res.Err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/rules/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/rules/delegate.go new file mode 100644 index 000000000000..23b4b318e2ba --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/rules/delegate.go @@ -0,0 +1,30 @@ +package rules + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns a Pager which allows you to iterate over a collection of +// security group rules. 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.ListOpts) pagination.Pager { + return os.List(c, opts) +} + +// Create is an operation which provisions a new security group with default +// security group rules for the IPv4 and IPv6 ether types. +func Create(c *gophercloud.ServiceClient, opts os.CreateOpts) os.CreateResult { + return os.Create(c, opts) +} + +// Get retrieves a particular security group based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) os.GetResult { + return os.Get(c, id) +} + +// Delete will permanently delete a particular security group based on its unique ID. +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/networking/v2/security/rules/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/rules/delegate_test.go new file mode 100644 index 000000000000..3563fbeaa6a6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/networking/v2/security/rules/delegate_test.go @@ -0,0 +1,236 @@ +package rules + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + osRules "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", 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_rules": [ + { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "direction": "egress", + "ethertype": "IPv4", + "id": "93aa42e5-80db-4581-9391-3a608bd0e448", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ] + } + `) + }) + + count := 0 + + List(fake.ServiceClient(), osRules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := osRules.ExtractRules(page) + if err != nil { + t.Errorf("Failed to extract secrules: %v", err) + return false, err + } + + expected := []osRules.SecGroupRule{ + osRules.SecGroupRule{ + Direction: "egress", + EtherType: "IPv6", + ID: "3c0e45ff-adaf-4124-b083-bf390e5482ff", + PortRangeMax: 0, + PortRangeMin: 0, + Protocol: "", + RemoteGroupID: "", + RemoteIPPrefix: "", + SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + osRules.SecGroupRule{ + Direction: "egress", + EtherType: "IPv4", + ID: "93aa42e5-80db-4581-9391-3a608bd0e448", + PortRangeMax: 0, + PortRangeMin: 0, + Protocol: "", + RemoteGroupID: "", + RemoteIPPrefix: "", + SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/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.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "security_group_rule": { + "direction": "ingress", + "port_range_min": 80, + "ethertype": "IPv4", + "port_range_max": 80, + "protocol": "tcp", + "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` + { + "security_group_rule": { + "direction": "ingress", + "ethertype": "IPv4", + "id": "2bc0accf-312e-429a-956e-e4407625eb62", + "port_range_max": 80, + "port_range_min": 80, + "protocol": "tcp", + "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "remote_ip_prefix": null, + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + } + `) + }) + + opts := osRules.CreateOpts{ + Direction: "ingress", + PortRangeMin: 80, + EtherType: "IPv4", + PortRangeMax: 80, + Protocol: "tcp", + RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + } + _, err := Create(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), osRules.CreateOpts{Direction: "something"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), osRules.CreateOpts{Direction: osRules.DirIngress, EtherType: "something"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), osRules.CreateOpts{Direction: osRules.DirIngress, EtherType: osRules.Ether4}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), osRules.CreateOpts{Direction: osRules.DirIngress, EtherType: osRules.Ether4, SecGroupID: "something", Protocol: "foo"}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/3c0e45ff-adaf-4124-b083-bf390e5482ff", 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_rule": { + "direction": "egress", + "ethertype": "IPv6", + "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff", + "port_range_max": null, + "port_range_min": null, + "protocol": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + } + `) + }) + + sr, err := Get(fake.ServiceClient(), "3c0e45ff-adaf-4124-b083-bf390e5482ff").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "egress", sr.Direction) + th.AssertEquals(t, "IPv6", sr.EtherType) + th.AssertEquals(t, "3c0e45ff-adaf-4124-b083-bf390e5482ff", sr.ID) + th.AssertEquals(t, 0, sr.PortRangeMax) + th.AssertEquals(t, 0, sr.PortRangeMin) + th.AssertEquals(t, "", sr.Protocol) + th.AssertEquals(t, "", sr.RemoteGroupID) + th.AssertEquals(t, "", sr.RemoteIPPrefix) + th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sr.SecGroupID) + th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sr.TenantID) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/4ec89087-d057-4e2c-911f-60a3b47ee304", 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(), "4ec89087-d057-4e2c-911f-60a3b47ee304") + 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 000000000000..a7fb7bb15fcf --- /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 000000000000..fafc6fb30205 --- /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.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate.go new file mode 100644 index 000000000000..94739308fa6c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate.go @@ -0,0 +1,39 @@ +package accounts + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" +) + +// Get is a function that retrieves an account's metadata. To extract just the +// custom metadata, call the ExtractMetadata method on the GetResult. To extract +// all the headers that are returned (including the metadata), call the +// ExtractHeader method on the GetResult. +func Get(c *gophercloud.ServiceClient) os.GetResult { + return os.Get(c, nil) +} + +// UpdateOpts is a structure that contains parameters for updating, creating, or +// deleting an account's metadata. +type UpdateOpts struct { + Metadata map[string]string + TempURLKey string `h:"X-Account-Meta-Temp-URL-Key"` + TempURLKey2 string `h:"X-Account-Meta-Temp-URL-Key-2"` +} + +// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers. +func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) { + headers, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + headers["X-Account-Meta-"+k] = v + } + return headers, err +} + +// Update will update an account's metadata with the Metadata in the UpdateOptsBuilder. +func Update(c *gophercloud.ServiceClient, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, opts) +} 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 new file mode 100644 index 000000000000..a1ea98bb7001 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/delegate_test.go @@ -0,0 +1,32 @@ +package accounts + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestUpdateAccounts(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.HandleUpdateAccountSuccessfully(t) + + options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}} + res := Update(fake.ServiceClient(), options) + th.CheckNoErr(t, res.Err) +} + +func TestGetAccounts(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + os.HandleGetAccountSuccessfully(t) + + expected := map[string]string{"Foo": "bar"} + actual, err := Get(fake.ServiceClient()).ExtractMetadata() + th.CheckNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/doc.go new file mode 100644 index 000000000000..293a93088a79 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts/doc.go @@ -0,0 +1,3 @@ +// Package accounts provides information and interaction with the account +// API resource for the Rackspace Cloud Files service. +package accounts diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/doc.go new file mode 100644 index 000000000000..9c89e22b21b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/doc.go @@ -0,0 +1,3 @@ +// Package bulk provides functionality for working with bulk operations in the +// Rackspace Cloud Files service. +package bulk diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go new file mode 100644 index 000000000000..898b73b0bd84 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests.go @@ -0,0 +1,49 @@ +package bulk + +import ( + "net/url" + "strings" + + "github.com/rackspace/gophercloud" +) + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToBulkDeleteBody() (string, error) +} + +// DeleteOpts is a structure that holds parameters for deleting an object. +type DeleteOpts []string + +// ToBulkDeleteBody formats a DeleteOpts into a request body. +func (opts DeleteOpts) ToBulkDeleteBody() (string, error) { + return url.QueryEscape(strings.Join(opts, "\n")), nil +} + +// Delete will delete objects or containers in bulk. +func Delete(c *gophercloud.ServiceClient, opts DeleteOptsBuilder) DeleteResult { + var res DeleteResult + + if opts == nil { + return res + } + + reqString, err := opts.ToBulkDeleteBody() + if err != nil { + res.Err = err + return res + } + + reqBody := strings.NewReader(reqString) + + resp, err := c.Request("DELETE", deleteURL(c), gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "text/plain"}, + OkCodes: []int{200}, + JSONBody: reqBody, + JSONResponse: &res.Body, + }) + res.Header = resp.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests_test.go new file mode 100644 index 000000000000..8b5578e91e87 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/requests_test.go @@ -0,0 +1,36 @@ +package bulk + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestBulkDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.AssertEquals(t, r.URL.RawQuery, "bulk-delete") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "Number Not Found": 1, + "Response Status": "200 OK", + "Errors": [], + "Number Deleted": 1, + "Response Body": "" + } + `) + }) + + options := DeleteOpts{"gophercloud-testcontainer1", "gophercloud-testcontainer2"} + actual, err := Delete(fake.ServiceClient(), options).ExtractBody() + th.AssertNoErr(t, err) + th.AssertEquals(t, actual.NumberDeleted, 1) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/results.go new file mode 100644 index 000000000000..fddc125ac639 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/results.go @@ -0,0 +1,28 @@ +package bulk + +import ( + "github.com/rackspace/gophercloud" + + "github.com/mitchellh/mapstructure" +) + +// DeleteResult represents the result of a bulk delete operation. +type DeleteResult struct { + gophercloud.Result +} + +// DeleteRespBody is the form of the response body returned by a bulk delete request. +type DeleteRespBody struct { + NumberNotFound int `mapstructure:"Number Not Found"` + ResponseStatus string `mapstructure:"Response Status"` + Errors []string `mapstructure:"Errors"` + NumberDeleted int `mapstructure:"Number Deleted"` + ResponseBody string `mapstructure:"Response Body"` +} + +// ExtractBody will extract the body returned by the bulk extract request. +func (dr DeleteResult) ExtractBody() (DeleteRespBody, error) { + var resp DeleteRespBody + err := mapstructure.Decode(dr.Body, &resp) + return resp, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls.go new file mode 100644 index 000000000000..2e112033beca --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls.go @@ -0,0 +1,11 @@ +package bulk + +import "github.com/rackspace/gophercloud" + +func deleteURL(c *gophercloud.ServiceClient) string { + return c.Endpoint + "?bulk-delete" +} + +func extractURL(c *gophercloud.ServiceClient, ext string) string { + return c.Endpoint + "?extract-archive=" + ext +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls_test.go new file mode 100644 index 000000000000..9169e52f16b6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk/urls_test.go @@ -0,0 +1,26 @@ +package bulk + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestDeleteURL(t *testing.T) { + actual := deleteURL(endpointClient()) + expected := endpoint + "?bulk-delete" + th.CheckEquals(t, expected, actual) +} + +func TestExtractURL(t *testing.T) { + actual := extractURL(endpointClient(), "tar") + expected := endpoint + "?extract-archive=tar" + th.CheckEquals(t, expected, actual) +} 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 new file mode 100644 index 000000000000..89adb83965d9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate.go @@ -0,0 +1,38 @@ +package cdncontainers + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractNames interprets a page of List results when just the container +// names are requested. +func ExtractNames(page pagination.Page) ([]string, error) { + return os.ExtractNames(page) +} + +// ListOpts are options for listing Rackspace CDN containers. +type ListOpts struct { + EndMarker string `q:"end_marker"` + Format string `q:"format"` + Limit int `q:"limit"` + Marker string `q:"marker"` +} + +// ToContainerListParams formats a ListOpts into a query string and boolean +// representing whether to list complete information for each container. +func (opts ListOpts) ToContainerListParams() (bool, string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return false, "", err + } + return false, q.String(), nil +} + +// List is a function that retrieves containers associated with the account as +// well as account metadata. It returns a pager which can be iterated with the +// EachPage function. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate_test.go new file mode 100644 index 000000000000..02c3c5e150ae --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/delegate_test.go @@ -0,0 +1,50 @@ +package cdncontainers + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListCDNContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListContainerNamesSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetCDNContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetContainerSuccessfully(t) + + _, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata() + th.CheckNoErr(t, err) + +} + +func TestUpdateCDNContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateContainerSuccessfully(t) + + options := &UpdateOpts{TTL: 3600} + res := Update(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) + +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/doc.go new file mode 100644 index 000000000000..7b0930eeea6c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/doc.go @@ -0,0 +1,3 @@ +// Package cdncontainers provides information and interaction with the CDN +// Container API resource for the Rackspace Cloud Files service. +package cdncontainers 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 new file mode 100644 index 000000000000..8e4abbe436c2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests.go @@ -0,0 +1,155 @@ +package cdncontainers + +import ( + "strconv" + + "github.com/rackspace/gophercloud" +) + +// EnableOptsBuilder allows extensions to add additional parameters to the Enable +// request. +type EnableOptsBuilder interface { + ToCDNContainerEnableMap() (map[string]string, error) +} + +// EnableOpts is a structure that holds options for enabling a CDN container. +type EnableOpts struct { + // CDNEnabled indicates whether or not the container is CDN enabled. Set to + // `true` to enable the container. Note that changing this setting from true + // to false will disable the container in the CDN but only after the TTL has + // expired. + CDNEnabled bool `h:"X-Cdn-Enabled"` + // TTL is the time-to-live for the container (in seconds). + TTL int `h:"X-Ttl"` +} + +// ToCDNContainerEnableMap formats an EnableOpts into a map of headers. +func (opts EnableOpts) ToCDNContainerEnableMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + return h, nil +} + +// Enable is a function that enables/disables a CDN container. +func Enable(c *gophercloud.ServiceClient, containerName string, opts EnableOptsBuilder) EnableResult { + var res EnableResult + h := c.AuthenticatedHeaders() + + if opts != nil { + headers, err := opts.ToCDNContainerEnableMap() + if err != nil { + res.Err = err + return res + } + + for k, v := range headers { + h[k] = v + } + } + + resp, err := c.Request("PUT", enableURL(c, containerName), gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201, 202, 204}, + }) + res.Header = resp.Header + 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 := c.Request("HEAD", getURL(c, containerName), gophercloud.RequestOpts{ + OkCodes: []int{200, 204}, + }) + res.Header = resp.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 := c.Request("POST", updateURL(c, containerName), gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{202, 204}, + }) + res.Header = resp.Header + res.Err = err + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests_test.go new file mode 100644 index 000000000000..28b963dacef1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/requests_test.go @@ -0,0 +1,29 @@ +package cdncontainers + +import ( + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestEnableCDNContainer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/testContainer", 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") + + w.Header().Add("X-Ttl", "259200") + w.Header().Add("X-Cdn-Enabled", "True") + w.WriteHeader(http.StatusNoContent) + }) + + options := &EnableOpts{CDNEnabled: true, TTL: 259200} + actual := Enable(fake.ServiceClient(), "testContainer", options) + th.AssertNoErr(t, actual.Err) + th.CheckEquals(t, actual.Header["X-Ttl"][0], "259200") + th.CheckEquals(t, actual.Header["X-Cdn-Enabled"][0], "True") +} 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 new file mode 100644 index 000000000000..cb0ad3096c3b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/results.go @@ -0,0 +1,149 @@ +package cdncontainers + +import ( + "strings" + "time" + + "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 new file mode 100644 index 000000000000..541249a92731 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls.go @@ -0,0 +1,15 @@ +package cdncontainers + +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/cdncontainers/urls_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls_test.go new file mode 100644 index 000000000000..aa5bfe68b29b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers/urls_test.go @@ -0,0 +1,20 @@ +package cdncontainers + +import ( + "testing" + + "github.com/rackspace/gophercloud" + th "github.com/rackspace/gophercloud/testhelper" +) + +const endpoint = "http://localhost:57909/" + +func endpointClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{Endpoint: endpoint} +} + +func TestEnableURL(t *testing.T) { + actual := enableURL(endpointClient(), "foo") + expected := endpoint + "foo" + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate.go new file mode 100644 index 000000000000..e9d2ff1d6f3f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate.go @@ -0,0 +1,11 @@ +package cdnobjects + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" +) + +// Delete is a function that deletes an object from the CDN. +func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DeleteOptsBuilder) os.DeleteResult { + return os.Delete(c, containerName, objectName, nil) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate_test.go new file mode 100644 index 000000000000..b5e04a98c3b9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/delegate_test.go @@ -0,0 +1,19 @@ +package cdnobjects + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDeleteCDNObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteObjectSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil) + th.AssertNoErr(t, res.Err) + +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/doc.go new file mode 100644 index 000000000000..90cd5c97ffcf --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/doc.go @@ -0,0 +1,3 @@ +// Package cdnobjects provides information and interaction with the CDN +// Object API resource for the Rackspace Cloud Files service. +package cdnobjects diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/request.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/request.go new file mode 100644 index 000000000000..540e0cd298a0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects/request.go @@ -0,0 +1,15 @@ +package cdnobjects + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers" +) + +// CDNURL returns the unique CDN URI for the given container and object. +func CDNURL(c *gophercloud.ServiceClient, containerName, objectName string) (string, error) { + h, err := cdncontainers.Get(c, containerName).Extract() + if err != nil { + return "", err + } + return h.CDNUri + "/" + objectName, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate.go new file mode 100644 index 000000000000..77ed0025743e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate.go @@ -0,0 +1,93 @@ +package containers + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractInfo interprets a page of List results when full container info +// is requested. +func ExtractInfo(page pagination.Page) ([]os.Container, error) { + return os.ExtractInfo(page) +} + +// ExtractNames interprets a page of List results when just the container +// names are requested. +func ExtractNames(page pagination.Page) ([]string, error) { + return os.ExtractNames(page) +} + +// List is a function that retrieves containers associated with the account as +// well as account metadata. It returns a pager which can be iterated with the +// EachPage function. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// CreateOpts is a structure that holds parameters for creating a container. +type CreateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerWrite string `h:"X-Container-Write"` + VersionsLocation string `h:"X-Versions-Location"` +} + +// ToContainerCreateMap formats a CreateOpts into a map of headers. +func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Container-Meta-"+k] = v + } + return h, nil +} + +// Create is a function that creates a new container. +func Create(c *gophercloud.ServiceClient, containerName string, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, containerName, opts) +} + +// Delete is a function that deletes a container. +func Delete(c *gophercloud.ServiceClient, containerName string) os.DeleteResult { + return os.Delete(c, containerName) +} + +// UpdateOpts is a structure that holds parameters for updating or creating a +// container's metadata. +type UpdateOpts struct { + Metadata map[string]string + ContainerRead string `h:"X-Container-Read"` + ContainerWrite string `h:"X-Container-Write"` + ContentType string `h:"Content-Type"` + DetectContentType bool `h:"X-Detect-Content-Type"` + RemoveVersionsLocation string `h:"X-Remove-Versions-Location"` + VersionsLocation string `h:"X-Versions-Location"` +} + +// 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 + } + for k, v := range opts.Metadata { + h["X-Container-Meta-"+k] = v + } + 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) +} + +// 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) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate_test.go new file mode 100644 index 000000000000..7ba4eb21c687 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/delegate_test.go @@ -0,0 +1,91 @@ +package containers + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListContainerInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListContainerInfoSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &os.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListContainerNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListContainerNamesSuccessfully(t) + + count := 0 + err := List(fake.ServiceClient(), &os.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, os.ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateContainerSuccessfully(t) + + options := os.CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}} + res := Create(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) + th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0]) + +} + +func TestDeleteContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteContainerSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer") + th.CheckNoErr(t, res.Err) +} + +func TestUpdateContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateContainerSuccessfully(t) + + options := &os.UpdateOpts{Metadata: map[string]string{"foo": "bar"}} + res := Update(fake.ServiceClient(), "testContainer", options) + th.CheckNoErr(t, res.Err) +} + +func TestGetContainers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetContainerSuccessfully(t) + + _, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata() + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/doc.go new file mode 100644 index 000000000000..d132a07382a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers/doc.go @@ -0,0 +1,3 @@ +// Package containers provides information and interaction with the Container +// API resource for the Rackspace Cloud Files service. +package containers 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 new file mode 100644 index 000000000000..028d66a0ab77 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate.go @@ -0,0 +1,94 @@ +package objects + +import ( + "io" + + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" +) + +// ExtractInfo is a function that takes a page of objects and returns their full information. +func ExtractInfo(page pagination.Page) ([]os.Object, error) { + return os.ExtractInfo(page) +} + +// ExtractNames is a function that takes a page of objects and returns only their names. +func ExtractNames(page pagination.Page) ([]string, error) { + return os.ExtractNames(page) +} + +// List is a function that retrieves objects in the container as +// well as container metadata. It returns a pager which can be iterated with the +// EachPage function. +func List(c *gophercloud.ServiceClient, containerName string, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, containerName, opts) +} + +// Download is a function that retrieves the content and metadata for an object. +// To extract just the content, pass the DownloadResult response to the +// ExtractContent function. +func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DownloadOptsBuilder) os.DownloadResult { + return os.Download(c, containerName, objectName, opts) +} + +// Create is a function that creates a new object or replaces an existing object. +func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, containerName, objectName, content, opts) +} + +// CopyOpts is a structure that holds parameters for copying one object to +// another. +type CopyOpts struct { + Metadata map[string]string + ContentDisposition string `h:"Content-Disposition"` + ContentEncoding string `h:"Content-Encoding"` + ContentLength int `h:"Content-Length"` + ContentType string `h:"Content-Type"` + CopyFrom string `h:"X-Copy_From"` + Destination string `h:"Destination"` + DetectContentType bool `h:"X-Detect-Content-Type"` +} + +// ToObjectCopyMap formats a CopyOpts into a map of headers. +func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) { + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + for k, v := range opts.Metadata { + h["X-Object-Meta-"+k] = v + } + // `Content-Length` is required and a value of "0" is acceptable, but calling `gophercloud.BuildHeaders` + // will remove the `Content-Length` header if it's set to 0 (or equivalently not set). This will add + // the header if it's not already set. + if _, ok := h["Content-Length"]; !ok { + h["Content-Length"] = "0" + } + return h, nil +} + +// Copy is a function that copies one object to another. +func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts os.CopyOptsBuilder) os.CopyResult { + return os.Copy(c, containerName, objectName, opts) +} + +// Delete is a function that deletes an object. +func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DeleteOptsBuilder) os.DeleteResult { + return os.Delete(c, containerName, objectName, opts) +} + +// Get is a function that retrieves the metadata of an object. To extract just the custom +// metadata, pass the GetResult response to the ExtractMetadata function. +func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts os.GetOptsBuilder) os.GetResult { + return os.Get(c, containerName, objectName, opts) +} + +// Update is a function that creates, updates, or deletes an object's metadata. +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 new file mode 100644 index 000000000000..8ab8029c37b5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/delegate_test.go @@ -0,0 +1,125 @@ +package objects + +import ( + "bytes" + "testing" + + os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestDownloadObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDownloadObjectSuccessfully(t) + + content, err := Download(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractContent() + th.AssertNoErr(t, err) + th.CheckEquals(t, string(content), "Successful download with Gophercloud") +} + +func TestListObjectsInfo(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListObjectsInfoSuccessfully(t) + + count := 0 + options := &os.ListOpts{Full: true} + err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractInfo(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ExpectedListInfo, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListObjectNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListObjectNamesSuccessfully(t) + + count := 0 + options := &os.ListOpts{Full: false} + err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, os.ExpectedListNames, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateTextObjectSuccessfully(t) + + content := bytes.NewBufferString("Did gyre and gimble in the wabe") + 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() + os.HandleCopyObjectSuccessfully(t) + + options := &CopyOpts{Destination: "/newTestContainer/newTestObject"} + res := Copy(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestDeleteObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteObjectSuccessfully(t) + + res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil) + th.AssertNoErr(t, res.Err) +} + +func TestUpdateObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateObjectSuccessfully(t) + + options := &os.UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}} + res := Update(fake.ServiceClient(), "testContainer", "testObject", options) + th.AssertNoErr(t, res.Err) +} + +func TestGetObject(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetObjectSuccessfully(t) + + expected := map[string]string{"Gophercloud-Test": "objects"} + actual, err := Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/doc.go new file mode 100644 index 000000000000..781984bee25c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects/doc.go @@ -0,0 +1,3 @@ +// Package objects provides information and interaction with the Object +// API resource for the Rackspace Cloud Files service. +package objects diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo/delegate.go new file mode 100644 index 000000000000..c834e5c7d39b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo/delegate.go @@ -0,0 +1,11 @@ +package buildinfo + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo" +) + +// Get retreives build info data for the Heat deployment. +func Get(c *gophercloud.ServiceClient) os.GetResult { + return os.Get(c) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo/delegate_test.go new file mode 100644 index 000000000000..b25a690c8dfd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo/delegate_test.go @@ -0,0 +1,21 @@ +package buildinfo + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestGetTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetSuccessfully(t, os.GetOutput) + + actual, err := Get(fake.ServiceClient()).Extract() + th.AssertNoErr(t, err) + + expected := os.GetExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo/doc.go new file mode 100644 index 000000000000..183e8dfa76d0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo/doc.go @@ -0,0 +1,2 @@ +// Package buildinfo provides build information about heat deployments. +package buildinfo diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents/delegate.go new file mode 100644 index 000000000000..08675deac779 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents/delegate.go @@ -0,0 +1,27 @@ +package stackevents + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents" + "github.com/rackspace/gophercloud/pagination" +) + +// Find retreives stack events for the given stack name. +func Find(c *gophercloud.ServiceClient, stackName string) os.FindResult { + return os.Find(c, stackName) +} + +// List makes a request against the API to list resources for the given stack. +func List(c *gophercloud.ServiceClient, stackName, stackID string, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, stackName, stackID, opts) +} + +// ListResourceEvents makes a request against the API to list resources for the given stack. +func ListResourceEvents(c *gophercloud.ServiceClient, stackName, stackID, resourceName string, opts os.ListResourceEventsOptsBuilder) pagination.Pager { + return os.ListResourceEvents(c, stackName, stackID, resourceName, opts) +} + +// Get retreives data for the given stack resource. +func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) os.GetResult { + return os.Get(c, stackName, stackID, resourceName, eventID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents/delegate_test.go new file mode 100644 index 000000000000..e1c0bc8dbcf0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents/delegate_test.go @@ -0,0 +1,72 @@ +package stackevents + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestFindEvents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleFindSuccessfully(t, os.FindOutput) + + actual, err := Find(fake.ServiceClient(), "postman_stack").Extract() + th.AssertNoErr(t, err) + + expected := os.FindExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListSuccessfully(t, os.ListOutput) + + count := 0 + err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractEvents(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ListExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListResourceEvents(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListResourceEventsSuccessfully(t, os.ListResourceEventsOutput) + + count := 0 + err := ListResourceEvents(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractEvents(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ListResourceEventsExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetEvent(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetSuccessfully(t, os.GetOutput) + + actual, err := Get(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", "93940999-7d40-44ae-8de4-19624e7b8d18").Extract() + th.AssertNoErr(t, err) + + expected := os.GetExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents/doc.go new file mode 100644 index 000000000000..dfd6ef6605ee --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents/doc.go @@ -0,0 +1,3 @@ +// Package stackevents provides operations for finding, listing, and retrieving +// stack events. +package stackevents diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources/delegate.go new file mode 100644 index 000000000000..cb7be28b78ae --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources/delegate.go @@ -0,0 +1,42 @@ +package stackresources + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources" + "github.com/rackspace/gophercloud/pagination" +) + +// Find retreives stack resources for the given stack name. +func Find(c *gophercloud.ServiceClient, stackName string) os.FindResult { + return os.Find(c, stackName) +} + +// List makes a request against the API to list resources for the given stack. +func List(c *gophercloud.ServiceClient, stackName, stackID string, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, stackName, stackID, opts) +} + +// Get retreives data for the given stack resource. +func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) os.GetResult { + return os.Get(c, stackName, stackID, resourceName) +} + +// Metadata retreives the metadata for the given stack resource. +func Metadata(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) os.MetadataResult { + return os.Metadata(c, stackName, stackID, resourceName) +} + +// ListTypes makes a request against the API to list resource types. +func ListTypes(c *gophercloud.ServiceClient) pagination.Pager { + return os.ListTypes(c) +} + +// Schema retreives the schema for the given resource type. +func Schema(c *gophercloud.ServiceClient, resourceType string) os.SchemaResult { + return os.Schema(c, resourceType) +} + +// Template retreives the template representation for the given resource type. +func Template(c *gophercloud.ServiceClient, resourceType string) os.TemplateResult { + return os.Template(c, resourceType) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources/delegate_test.go new file mode 100644 index 000000000000..18e9614151ad --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources/delegate_test.go @@ -0,0 +1,108 @@ +package stackresources + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestFindResources(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleFindSuccessfully(t, os.FindOutput) + + actual, err := Find(fake.ServiceClient(), "hello_world").Extract() + th.AssertNoErr(t, err) + + expected := os.FindExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestListResources(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListSuccessfully(t, os.ListOutput) + + count := 0 + err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractResources(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ListExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetResource(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetSuccessfully(t, os.GetOutput) + + actual, err := Get(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract() + th.AssertNoErr(t, err) + + expected := os.GetExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestResourceMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleMetadataSuccessfully(t, os.MetadataOutput) + + actual, err := Metadata(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract() + th.AssertNoErr(t, err) + + expected := os.MetadataExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestListResourceTypes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListTypesSuccessfully(t, os.ListTypesOutput) + + count := 0 + err := ListTypes(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractResourceTypes(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ListTypesExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 1, count) +} + +func TestGetResourceSchema(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetSchemaSuccessfully(t, os.GetSchemaOutput) + + actual, err := Schema(fake.ServiceClient(), "OS::Heat::AResourceName").Extract() + th.AssertNoErr(t, err) + + expected := os.GetSchemaExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestGetResourceTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetTemplateSuccessfully(t, os.GetTemplateOutput) + + actual, err := Template(fake.ServiceClient(), "OS::Heat::AResourceName").Extract() + th.AssertNoErr(t, err) + + expected := os.GetTemplateExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources/doc.go new file mode 100644 index 000000000000..e4f8b08dcc7e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources/doc.go @@ -0,0 +1,5 @@ +// Package stackresources provides operations for working with stack resources. +// A resource is a template artifact that represents some component of your +// desired architecture (a Cloud Server, a group of scaled Cloud Servers, a load +// balancer, some configuration management system, and so forth). +package stackresources diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/delegate.go new file mode 100644 index 000000000000..f7e387f8f792 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/delegate.go @@ -0,0 +1,49 @@ +package stacks + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" + "github.com/rackspace/gophercloud/pagination" +) + +// Create accepts an os.CreateOpts struct and creates a new stack using the values +// provided. +func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult { + return os.Create(c, opts) +} + +// Adopt accepts an os.AdoptOpts struct and creates a new stack from existing stack +// resources using the values provided. +func Adopt(c *gophercloud.ServiceClient, opts os.AdoptOptsBuilder) os.AdoptResult { + return os.Adopt(c, opts) +} + +// List accepts an os.ListOpts struct and lists stacks based on the options provided. +func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager { + return os.List(c, opts) +} + +// Get retreives a stack based on the stack name and stack ID. +func Get(c *gophercloud.ServiceClient, stackName, stackID string) os.GetResult { + return os.Get(c, stackName, stackID) +} + +// Update accepts an os.UpdateOpts struct and updates a stack based on the options provided. +func Update(c *gophercloud.ServiceClient, stackName, stackID string, opts os.UpdateOptsBuilder) os.UpdateResult { + return os.Update(c, stackName, stackID, opts) +} + +// Delete deletes a stack based on the stack name and stack ID provided. +func Delete(c *gophercloud.ServiceClient, stackName, stackID string) os.DeleteResult { + return os.Delete(c, stackName, stackID) +} + +// Preview provides a preview of a stack based on the options provided. +func Preview(c *gophercloud.ServiceClient, opts os.PreviewOptsBuilder) os.PreviewResult { + return os.Preview(c, opts) +} + +// Abandon abandons a stack, keeping the resources available. +func Abandon(c *gophercloud.ServiceClient, stackName, stackID string) os.AbandonResult { + return os.Abandon(c, stackName, stackID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/delegate_test.go new file mode 100644 index 000000000000..a1fb3931299b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/delegate_test.go @@ -0,0 +1,461 @@ +package stacks + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestCreateStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateSuccessfully(t, CreateOutput) + + createOpts := os.CreateOpts{ + Name: "stackcreated", + Timeout: 60, + Template: `{ + "outputs": { + "db_host": { + "value": { + "get_attr": [ + "db", + "hostname" + ] + } + } + }, + "heat_template_version": "2014-10-16", + "description": "HEAT template for creating a Cloud Database.\n", + "parameters": { + "db_name": { + "default": "wordpress", + "type": "string", + "description": "the name for the database", + "constraints": [ + { + "length": { + "max": 64, + "min": 1 + }, + "description": "must be between 1 and 64 characters" + }, + { + "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*", + "description": "must begin with a letter and contain only alphanumeric characters." + } + ] + }, + "db_instance_name": { + "default": "Cloud_DB", + "type": "string", + "description": "the database instance name" + }, + "db_username": { + "default": "admin", + "hidden": true, + "type": "string", + "description": "database admin account username", + "constraints": [ + { + "length": { + "max": 16, + "min": 1 + }, + "description": "must be between 1 and 16 characters" + }, + { + "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*", + "description": "must begin with a letter and contain only alphanumeric characters." + } + ] + }, + "db_volume_size": { + "default": 30, + "type": "number", + "description": "database volume size (in GB)", + "constraints": [ + { + "range": { + "max": 1024, + "min": 1 + }, + "description": "must be between 1 and 1024 GB" + } + ] + }, + "db_flavor": { + "default": "1GB Instance", + "type": "string", + "description": "database instance size", + "constraints": [ + { + "description": "must be a valid cloud database flavor", + "allowed_values": [ + "1GB Instance", + "2GB Instance", + "4GB Instance", + "8GB Instance", + "16GB Instance" + ] + } + ] + }, + "db_password": { + "default": "admin", + "hidden": true, + "type": "string", + "description": "database admin account password", + "constraints": [ + { + "length": { + "max": 41, + "min": 1 + }, + "description": "must be between 1 and 14 characters" + }, + { + "allowed_pattern": "[a-zA-Z0-9]*", + "description": "must contain only alphanumeric characters." + } + ] + } + }, + "resources": { + "db": { + "type": "OS::Trove::Instance", + "properties": { + "flavor": { + "get_param": "db_flavor" + }, + "size": { + "get_param": "db_volume_size" + }, + "users": [ + { + "password": { + "get_param": "db_password" + }, + "name": { + "get_param": "db_username" + }, + "databases": [ + { + "get_param": "db_name" + } + ] + } + ], + "name": { + "get_param": "db_instance_name" + }, + "databases": [ + { + "name": { + "get_param": "db_name" + } + } + ] + } + } + } + }`, + DisableRollback: os.Disable, + } + actual, err := Create(fake.ServiceClient(), createOpts).Extract() + th.AssertNoErr(t, err) + + expected := CreateExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestAdoptStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleCreateSuccessfully(t, CreateOutput) + + adoptOpts := os.AdoptOpts{ + AdoptStackData: `{\"environment\":{\"parameters\":{}}, \"status\":\"COMPLETE\",\"name\": \"trovestack\",\n \"template\": {\n \"outputs\": {\n \"db_host\": {\n \"value\": {\n \"get_attr\": [\n \"db\",\n \"hostname\"\n ]\n }\n }\n },\n \"heat_template_version\": \"2014-10-16\",\n \"description\": \"HEAT template for creating a Cloud Database.\\n\",\n \"parameters\": {\n \"db_instance_name\": {\n \"default\": \"Cloud_DB\",\n \"type\": \"string\",\n \"description\": \"the database instance name\"\n },\n \"db_flavor\": {\n \"default\": \"1GB Instance\",\n \"type\": \"string\",\n \"description\": \"database instance size\",\n \"constraints\": [\n {\n \"description\": \"must be a valid cloud database flavor\",\n \"allowed_values\": [\n \"1GB Instance\",\n \"2GB Instance\",\n \"4GB Instance\",\n \"8GB Instance\",\n \"16GB Instance\"\n ]\n }\n ]\n },\n \"db_password\": {\n \"default\": \"admin\",\n \"hidden\": true,\n \"type\": \"string\",\n \"description\": \"database admin account password\",\n \"constraints\": [\n {\n \"length\": {\n \"max\": 41,\n \"min\": 1\n },\n \"description\": \"must be between 1 and 14 characters\"\n },\n {\n \"allowed_pattern\": \"[a-zA-Z0-9]*\",\n \"description\": \"must contain only alphanumeric characters.\"\n }\n ]\n },\n \"db_name\": {\n \"default\": \"wordpress\",\n \"type\": \"string\",\n \"description\": \"the name for the database\",\n \"constraints\": [\n {\n \"length\": {\n \"max\": 64,\n \"min\": 1\n },\n \"description\": \"must be between 1 and 64 characters\"\n },\n {\n \"allowed_pattern\": \"[a-zA-Z][a-zA-Z0-9]*\",\n \"description\": \"must begin with a letter and contain only alphanumeric characters.\"\n }\n ]\n },\n \"db_username\": {\n \"default\": \"admin\",\n \"hidden\": true,\n \"type\": \"string\",\n \"description\": \"database admin account username\",\n \"constraints\": [\n {\n \"length\": {\n \"max\": 16,\n \"min\": 1\n },\n \"description\": \"must be between 1 and 16 characters\"\n },\n {\n \"allowed_pattern\": \"[a-zA-Z][a-zA-Z0-9]*\",\n \"description\": \"must begin with a letter and contain only alphanumeric characters.\"\n }\n ]\n },\n \"db_volume_size\": {\n \"default\": 30,\n \"type\": \"number\",\n \"description\": \"database volume size (in GB)\",\n \"constraints\": [\n {\n \"range\": {\n \"max\": 1024,\n \"min\": 1\n },\n \"description\": \"must be between 1 and 1024 GB\"\n }\n ]\n }\n },\n \"resources\": {\n \"db\": {\n \"type\": \"OS::Trove::Instance\",\n \"properties\": {\n \"flavor\": {\n \"get_param\": \"db_flavor\"\n },\n \"databases\": [\n {\n \"name\": {\n \"get_param\": \"db_name\"\n }\n }\n ],\n \"users\": [\n {\n \"password\": {\n \"get_param\": \"db_password\"\n },\n \"name\": {\n \"get_param\": \"db_username\"\n },\n \"databases\": [\n {\n \"get_param\": \"db_name\"\n }\n ]\n }\n ],\n \"name\": {\n \"get_param\": \"db_instance_name\"\n },\n \"size\": {\n \"get_param\": \"db_volume_size\"\n }\n }\n }\n }\n },\n \"action\": \"CREATE\",\n \"id\": \"exxxxd-7xx5-4xxb-bxx2-cxxxxxx5\",\n \"resources\": {\n \"db\": {\n \"status\": \"COMPLETE\",\n \"name\": \"db\",\n \"resource_data\": {},\n \"resource_id\": \"exxxx2-9xx0-4xxxb-bxx2-dxxxxxx4\",\n \"action\": \"CREATE\",\n \"type\": \"OS::Trove::Instance\",\n \"metadata\": {}\n }\n }\n},`, + Name: "stackadopted", + Timeout: 60, + Template: `{ + "outputs": { + "db_host": { + "value": { + "get_attr": [ + "db", + "hostname" + ] + } + } + }, + "heat_template_version": "2014-10-16", + "description": "HEAT template for creating a Cloud Database.\n", + "parameters": { + "db_name": { + "default": "wordpress", + "type": "string", + "description": "the name for the database", + "constraints": [ + { + "length": { + "max": 64, + "min": 1 + }, + "description": "must be between 1 and 64 characters" + }, + { + "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*", + "description": "must begin with a letter and contain only alphanumeric characters." + } + ] + }, + "db_instance_name": { + "default": "Cloud_DB", + "type": "string", + "description": "the database instance name" + }, + "db_username": { + "default": "admin", + "hidden": true, + "type": "string", + "description": "database admin account username", + "constraints": [ + { + "length": { + "max": 16, + "min": 1 + }, + "description": "must be between 1 and 16 characters" + }, + { + "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*", + "description": "must begin with a letter and contain only alphanumeric characters." + } + ] + }, + "db_volume_size": { + "default": 30, + "type": "number", + "description": "database volume size (in GB)", + "constraints": [ + { + "range": { + "max": 1024, + "min": 1 + }, + "description": "must be between 1 and 1024 GB" + } + ] + }, + "db_flavor": { + "default": "1GB Instance", + "type": "string", + "description": "database instance size", + "constraints": [ + { + "description": "must be a valid cloud database flavor", + "allowed_values": [ + "1GB Instance", + "2GB Instance", + "4GB Instance", + "8GB Instance", + "16GB Instance" + ] + } + ] + }, + "db_password": { + "default": "admin", + "hidden": true, + "type": "string", + "description": "database admin account password", + "constraints": [ + { + "length": { + "max": 41, + "min": 1 + }, + "description": "must be between 1 and 14 characters" + }, + { + "allowed_pattern": "[a-zA-Z0-9]*", + "description": "must contain only alphanumeric characters." + } + ] + } + }, + "resources": { + "db": { + "type": "OS::Trove::Instance", + "properties": { + "flavor": { + "get_param": "db_flavor" + }, + "size": { + "get_param": "db_volume_size" + }, + "users": [ + { + "password": { + "get_param": "db_password" + }, + "name": { + "get_param": "db_username" + }, + "databases": [ + { + "get_param": "db_name" + } + ] + } + ], + "name": { + "get_param": "db_instance_name" + }, + "databases": [ + { + "name": { + "get_param": "db_name" + } + } + ] + } + } + } + }`, + DisableRollback: os.Disable, + } + actual, err := Adopt(fake.ServiceClient(), adoptOpts).Extract() + th.AssertNoErr(t, err) + + expected := CreateExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestListStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleListSuccessfully(t, os.FullListOutput) + + count := 0 + err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := os.ExtractStacks(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, os.ListExpected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestUpdateStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleUpdateSuccessfully(t) + + updateOpts := os.UpdateOpts{ + Template: ` + { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type":"OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + }`, + } + err := Update(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada", updateOpts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDeleteStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleDeleteSuccessfully(t) + + err := Delete(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestPreviewStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandlePreviewSuccessfully(t, os.GetOutput) + + previewOpts := os.PreviewOpts{ + Name: "stackcreated", + Timeout: 60, + Template: ` + { + "stack_name": "postman_stack", + "template": { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type":"OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + } + }`, + DisableRollback: os.Disable, + } + actual, err := Preview(fake.ServiceClient(), previewOpts).Extract() + th.AssertNoErr(t, err) + + expected := os.PreviewExpected + th.AssertDeepEquals(t, expected, actual) +} + +/* +func TestAbandonStack(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleAbandonSuccessfully(t) + + //actual, err := Abandon(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract() + //th.AssertNoErr(t, err) + res := Abandon(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87") //.Extract() + th.AssertNoErr(t, res.Err) + t.Logf("actual: %+v", res) + + //expected := os.AbandonExpected + //th.AssertDeepEquals(t, expected, actual) +} +*/ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/doc.go new file mode 100644 index 000000000000..19231b5137e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/doc.go @@ -0,0 +1,8 @@ +// Package stacks provides operation for working with Heat stacks. A stack is a +// group of resources (servers, load balancers, databases, and so forth) +// combined to fulfill a useful purpose. Based on a template, Heat orchestration +// engine creates an instantiated set of resources (a stack) to run the +// application framework or component specified (in the template). A stack is a +// running instance of a template. The result of creating a stack is a deployment +// of the application framework or component. +package stacks diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/fixtures.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/fixtures.go new file mode 100644 index 000000000000..c9afeb156f62 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks/fixtures.go @@ -0,0 +1,32 @@ +package stacks + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks" +) + +// CreateExpected represents the expected object from a Create request. +var CreateExpected = &os.CreatedStack{ + ID: "b663e18a-4767-4cdf-9db5-9c8cc13cc38a", + Links: []gophercloud.Link{ + gophercloud.Link{ + Href: "https://ord.orchestration.api.rackspacecloud.com/v1/864477/stacks/stackcreated/b663e18a-4767-4cdf-9db5-9c8cc13cc38a", + Rel: "self", + }, + }, +} + +// CreateOutput represents the response body from a Create request. +const CreateOutput = ` +{ + "stack": { + "id": "b663e18a-4767-4cdf-9db5-9c8cc13cc38a", + "links": [ + { + "href": "https://ord.orchestration.api.rackspacecloud.com/v1/864477/stacks/stackcreated/b663e18a-4767-4cdf-9db5-9c8cc13cc38a", + "rel": "self" + } + ] + } +} +` diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates/delegate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates/delegate.go new file mode 100644 index 000000000000..3b5d46e1c9e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates/delegate.go @@ -0,0 +1,16 @@ +package stacktemplates + +import ( + "github.com/rackspace/gophercloud" + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates" +) + +// Get retreives data for the given stack template. +func Get(c *gophercloud.ServiceClient, stackName, stackID string) os.GetResult { + return os.Get(c, stackName, stackID) +} + +// Validate validates the given stack template. +func Validate(c *gophercloud.ServiceClient, opts os.ValidateOptsBuilder) os.ValidateResult { + return os.Validate(c, opts) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates/delegate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates/delegate_test.go new file mode 100644 index 000000000000..d4006c476d5d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates/delegate_test.go @@ -0,0 +1,58 @@ +package stacktemplates + +import ( + "testing" + + os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestGetTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleGetSuccessfully(t, os.GetOutput) + + actual, err := Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract() + th.AssertNoErr(t, err) + + expected := os.GetExpected + th.AssertDeepEquals(t, expected, actual) +} + +func TestValidateTemplate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + os.HandleValidateSuccessfully(t, os.ValidateOutput) + + opts := os.ValidateOpts{ + Template: map[string]interface{}{ + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": map[string]interface{}{ + "flavor": map[string]interface{}{ + "default": "m1.tiny", + "type": "string", + }, + }, + "resources": map[string]interface{}{ + "hello_world": map[string]interface{}{ + "type": "OS::Nova::Server", + "properties": map[string]interface{}{ + "key_name": "heat_key", + "flavor": map[string]interface{}{ + "get_param": "flavor", + }, + "image": "ad091b52-742f-469e-8f3c-fd81cadf0743", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n", + }, + }, + }, + }, + } + actual, err := Validate(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + + expected := os.ValidateExpected + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates/doc.go new file mode 100644 index 000000000000..5af0bd62a113 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates/doc.go @@ -0,0 +1,8 @@ +// Package stacktemplates provides operations for working with Heat templates. +// A Cloud Orchestration template is a portable file, written in a user-readable +// language, that describes how a set of resources should be assembled and what +// software should be installed in order to produce a working stack. The template +// specifies what resources should be used, what attributes can be set, and other +// parameters that are critical to the successful, repeatable automation of a +// specific application stack. +package stacktemplates diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/requests.go new file mode 100644 index 000000000000..58843030aed9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/requests.go @@ -0,0 +1,24 @@ +package cloudnetworks + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns all cloud networks that are associated with RackConnect. The ID +// returned for each network is the same as the ID returned by the networks package. +func List(c *gophercloud.ServiceClient) pagination.Pager { + url := listURL(c) + createPage := func(r pagination.PageResult) pagination.Page { + return CloudNetworkPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} + +// Get retrieves a specific cloud network (that is associated with RackConnect) +// based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c, id), &res.Body, nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/requests_test.go new file mode 100644 index 000000000000..10d15dd11f71 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/requests_test.go @@ -0,0 +1,87 @@ +package cloudnetworks + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListCloudNetworks(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/cloud_networks", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `[{ + "cidr": "192.168.100.0/24", + "created": "2014-05-25T01:23:42Z", + "id": "07426958-1ebf-4c38-b032-d456820ca21a", + "name": "RC-CLOUD", + "updated": "2014-05-25T02:28:44Z" + }]`) + }) + + expected := []CloudNetwork{ + CloudNetwork{ + CIDR: "192.168.100.0/24", + CreatedAt: time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC), + ID: "07426958-1ebf-4c38-b032-d456820ca21a", + Name: "RC-CLOUD", + UpdatedAt: time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC), + }, + } + + count := 0 + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractCloudNetworks(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetCloudNetwork(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/cloud_networks/07426958-1ebf-4c38-b032-d456820ca21a", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "cidr": "192.168.100.0/24", + "created": "2014-05-25T01:23:42Z", + "id": "07426958-1ebf-4c38-b032-d456820ca21a", + "name": "RC-CLOUD", + "updated": "2014-05-25T02:28:44Z" + }`) + }) + + expected := &CloudNetwork{ + CIDR: "192.168.100.0/24", + CreatedAt: time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC), + ID: "07426958-1ebf-4c38-b032-d456820ca21a", + Name: "RC-CLOUD", + UpdatedAt: time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC), + } + + actual, err := Get(fake.ServiceClient(), "07426958-1ebf-4c38-b032-d456820ca21a").Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/results.go new file mode 100644 index 000000000000..f554a0d75bd6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/results.go @@ -0,0 +1,113 @@ +package cloudnetworks + +import ( + "fmt" + "reflect" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// CloudNetwork represents a network associated with a RackConnect configuration. +type CloudNetwork struct { + // Specifies the ID of the newtork. + ID string `mapstructure:"id"` + // Specifies the user-provided name of the network. + Name string `mapstructure:"name"` + // Specifies the IP range for this network. + CIDR string `mapstructure:"cidr"` + // Specifies the time the network was created. + CreatedAt time.Time `mapstructure:"-"` + // Specifies the time the network was last updated. + UpdatedAt time.Time `mapstructure:"-"` +} + +// CloudNetworkPage is the page returned by a pager when traversing over a +// collection of CloudNetworks. +type CloudNetworkPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a CloudNetworkPage contains no CloudNetworks. +func (r CloudNetworkPage) IsEmpty() (bool, error) { + cns, err := ExtractCloudNetworks(r) + if err != nil { + return true, err + } + return len(cns) == 0, nil +} + +// ExtractCloudNetworks extracts and returns CloudNetworks. It is used while iterating over +// a cloudnetworks.List call. +func ExtractCloudNetworks(page pagination.Page) ([]CloudNetwork, error) { + var res []CloudNetwork + casted := page.(CloudNetworkPage).Body + err := mapstructure.Decode(casted, &res) + + var rawNets []interface{} + switch casted.(type) { + case interface{}: + rawNets = casted.([]interface{}) + default: + return res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted)) + } + + for i := range rawNets { + thisNet := (rawNets[i]).(map[string]interface{}) + + if t, ok := thisNet["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CreatedAt = creationTime + } + + if t, ok := thisNet["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].UpdatedAt = updatedTime + } + } + + return res, err +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that extracts a CloudNetwork from a GetResult. +func (r GetResult) Extract() (*CloudNetwork, error) { + if r.Err != nil { + return nil, r.Err + } + var res CloudNetwork + + err := mapstructure.Decode(r.Body, &res) + + b := r.Body.(map[string]interface{}) + + if date, ok := b["created"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.CreatedAt = t + } + + if date, ok := b["updated"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.UpdatedAt = t + } + + return &res, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/urls.go new file mode 100644 index 000000000000..bd6b098dadc2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks/urls.go @@ -0,0 +1,11 @@ +package cloudnetworks + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("cloud_networks") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("cloud_networks", id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/doc.go new file mode 100644 index 000000000000..3a8279e10913 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/doc.go @@ -0,0 +1,4 @@ +// Package rackconnect allows Rackspace cloud accounts to leverage version 3 of +// RackConnect, Rackspace's hybrid connectivity solution connecting dedicated +// and cloud servers. +package rackconnect diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/doc.go new file mode 100644 index 000000000000..f4319b8ff37d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/doc.go @@ -0,0 +1,14 @@ +// Package lbpools provides access to load balancer pools associated with a +// RackConnect configuration. Load Balancer Pools must be configured in advance +// by your Network Security team to be eligible for use with RackConnect. +// If you do not see a pool that you expect to see, contact your Support team +// for further assistance. The Load Balancer Pool id returned by these calls is +// automatically generated by the RackConnect automation and will remain constant +// unless the Load Balancer Pool is renamed on your hardware load balancer. +// All Load Balancer Pools will currently return a status of ACTIVE. Future +// features may introduce additional statuses. +// Node status values are ADDING, ACTIVE, REMOVING, ADD_FAILED, and REMOVE_FAILED. +// The cloud_servers node count will only include Cloud Servers from the specified +// cloud account. Any dedicated servers or cloud servers from another cloud account +// on the same RackConnect Configuration will be counted as external nodes. +package lbpools diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/requests.go new file mode 100644 index 000000000000..c300c56c1e9a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/requests.go @@ -0,0 +1,146 @@ +package lbpools + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns all load balancer pools that are associated with RackConnect. +func List(c *gophercloud.ServiceClient) pagination.Pager { + url := listURL(c) + createPage := func(r pagination.PageResult) pagination.Page { + return PoolPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} + +// Get retrieves a specific load balancer pool (that is associated with RackConnect) +// based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c, id), &res.Body, nil) + return res +} + +// ListNodes returns all load balancer pool nodes that are associated with RackConnect +// for the given LB pool ID. +func ListNodes(c *gophercloud.ServiceClient, id string) pagination.Pager { + url := listNodesURL(c, id) + createPage := func(r pagination.PageResult) pagination.Page { + return NodePage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} + +// CreateNode adds the cloud server with the given serverID to the load balancer +// pool with the given poolID. +func CreateNode(c *gophercloud.ServiceClient, poolID, serverID string) CreateNodeResult { + var res CreateNodeResult + reqBody := map[string]interface{}{ + "cloud_server": map[string]string{ + "id": serverID, + }, + } + _, res.Err = c.Post(createNodeURL(c, poolID), reqBody, &res.Body, nil) + return res +} + +// ListNodesDetails returns all load balancer pool nodes that are associated with RackConnect +// for the given LB pool ID with all their details. +func ListNodesDetails(c *gophercloud.ServiceClient, id string) pagination.Pager { + url := listNodesDetailsURL(c, id) + createPage := func(r pagination.PageResult) pagination.Page { + return NodeDetailsPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} + +// GetNode retrieves a specific LB pool node (that is associated with RackConnect) +// based on its unique ID and the LB pool's unique ID. +func GetNode(c *gophercloud.ServiceClient, poolID, nodeID string) GetNodeResult { + var res GetNodeResult + _, res.Err = c.Get(nodeURL(c, poolID, nodeID), &res.Body, nil) + return res +} + +// DeleteNode removes the node with the given nodeID from the LB pool with the +// given poolID. +func DeleteNode(c *gophercloud.ServiceClient, poolID, nodeID string) DeleteNodeResult { + var res DeleteNodeResult + _, res.Err = c.Delete(deleteNodeURL(c, poolID, nodeID), nil) + return res +} + +// GetNodeDetails retrieves a specific LB pool node's details based on its unique +// ID and the LB pool's unique ID. +func GetNodeDetails(c *gophercloud.ServiceClient, poolID, nodeID string) GetNodeDetailsResult { + var res GetNodeDetailsResult + _, res.Err = c.Get(nodeDetailsURL(c, poolID, nodeID), &res.Body, nil) + return res +} + +// NodeOpts are options for bulk adding/deleting nodes to LB pools. +type NodeOpts struct { + ServerID string + PoolID string +} + +// NodesOpts are a slice of NodeOpts, passed as options for bulk operations. +type NodesOpts []NodeOpts + +// ToLBPoolCreateNodesMap serializes a NodesOpts into a map to send in the request. +func (o NodesOpts) ToLBPoolCreateNodesMap() ([]map[string]interface{}, error) { + m := make([]map[string]interface{}, len(o)) + for i := range o { + m[i] = map[string]interface{}{ + "cloud_server": map[string]string{ + "id": o[i].ServerID, + }, + "load_balancer_pool": map[string]string{ + "id": o[i].PoolID, + }, + } + } + return m, nil +} + +// CreateNodes adds the cloud servers with the given serverIDs to the corresponding +// load balancer pools with the given poolIDs. +func CreateNodes(c *gophercloud.ServiceClient, opts NodesOpts) CreateNodesResult { + var res CreateNodesResult + reqBody, err := opts.ToLBPoolCreateNodesMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Post(createNodesURL(c), reqBody, &res.Body, nil) + return res +} + +// DeleteNodes removes the cloud servers with the given serverIDs to the corresponding +// load balancer pools with the given poolIDs. +func DeleteNodes(c *gophercloud.ServiceClient, opts NodesOpts) DeleteNodesResult { + var res DeleteNodesResult + reqBody, err := opts.ToLBPoolCreateNodesMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = c.Request("DELETE", createNodesURL(c), gophercloud.RequestOpts{ + JSONBody: &reqBody, + OkCodes: []int{204}, + }) + return res +} + +// ListNodesDetailsForServer is similar to ListNodesDetails but only returns nodes +// for the given serverID. +func ListNodesDetailsForServer(c *gophercloud.ServiceClient, serverID string) pagination.Pager { + url := listNodesForServerURL(c, serverID) + createPage := func(r pagination.PageResult) pagination.Page { + return NodeDetailsForServerPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/requests_test.go new file mode 100644 index 000000000000..48ebcece1315 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/requests_test.go @@ -0,0 +1,876 @@ +package lbpools + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListPools(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `[ + { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + "name": "RCv3Test", + "node_counts": { + "cloud_servers": 3, + "external": 4, + "total": 7 + }, + "port": 80, + "status": "ACTIVE", + "status_detail": null, + "virtual_ip": "203.0.113.5" + }, + { + "id": "33021100-4abf-4836-9080-465a6d87ab68", + "name": "RCv3Test2", + "node_counts": { + "cloud_servers": 1, + "external": 0, + "total": 1 + }, + "port": 80, + "status": "ACTIVE", + "status_detail": null, + "virtual_ip": "203.0.113.7" + }, + { + "id": "b644350a-301b-47b5-a411-c6e0f933c347", + "name": "RCv3Test3", + "node_counts": { + "cloud_servers": 2, + "external": 3, + "total": 5 + }, + "port": 443, + "status": "ACTIVE", + "status_detail": null, + "virtual_ip": "203.0.113.15" + } + ]`) + }) + + expected := []Pool{ + Pool{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + Name: "RCv3Test", + NodeCounts: struct { + CloudServers int `mapstructure:"cloud_servers"` + External int `mapstructure:"external"` + Total int `mapstructure:"total"` + }{ + CloudServers: 3, + External: 4, + Total: 7, + }, + Port: 80, + Status: "ACTIVE", + VirtualIP: "203.0.113.5", + }, + Pool{ + ID: "33021100-4abf-4836-9080-465a6d87ab68", + Name: "RCv3Test2", + NodeCounts: struct { + CloudServers int `mapstructure:"cloud_servers"` + External int `mapstructure:"external"` + Total int `mapstructure:"total"` + }{ + CloudServers: 1, + External: 0, + Total: 1, + }, + Port: 80, + Status: "ACTIVE", + VirtualIP: "203.0.113.7", + }, + Pool{ + ID: "b644350a-301b-47b5-a411-c6e0f933c347", + Name: "RCv3Test3", + NodeCounts: struct { + CloudServers int `mapstructure:"cloud_servers"` + External int `mapstructure:"external"` + Total int `mapstructure:"total"` + }{ + CloudServers: 2, + External: 3, + Total: 5, + }, + Port: 443, + Status: "ACTIVE", + VirtualIP: "203.0.113.15", + }, + } + + count := 0 + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractPools(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetLBPool(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + "name": "RCv3Test", + "node_counts": { + "cloud_servers": 3, + "external": 4, + "total": 7 + }, + "port": 80, + "status": "ACTIVE", + "status_detail": null, + "virtual_ip": "203.0.113.5" + }`) + }) + + expected := &Pool{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + Name: "RCv3Test", + NodeCounts: struct { + CloudServers int `mapstructure:"cloud_servers"` + External int `mapstructure:"external"` + Total int `mapstructure:"total"` + }{ + CloudServers: 3, + External: 4, + Total: 7, + }, + Port: 80, + Status: "ACTIVE", + VirtualIP: "203.0.113.5", + } + + actual, err := Get(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2").Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expected, actual) +} + +func TestListNodes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `[ + { + "created": "2014-05-30T03:23:42Z", + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + }, + "id": "1860451d-fb89-45b8-b54e-151afceb50e5", + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2" + }, + "status": "ACTIVE", + "updated": "2014-05-30T03:24:18Z" + }, + { + "created": "2014-05-31T08:23:12Z", + "cloud_server": { + "id": "f28b870f-a063-498a-8b12-7025e5b1caa6" + }, + "id": "b70481dd-7edf-4dbb-a44b-41cc7679d4fb", + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2" + }, + "status": "ADDING", + "updated": "2014-05-31T08:23:26Z" + }, + { + "created": "2014-05-31T08:23:18Z", + "cloud_server": { + "id": "a3d3a6b3-e4e4-496f-9a3d-5c987163e458" + }, + "id": "ced9ddc8-6fae-4e72-9457-16ead52b5515", + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2" + }, + "status": "ADD_FAILED", + "status_detail": "Unable to communicate with network device", + "updated": "2014-05-31T08:24:36Z" + } + ]`) + }) + + expected := []Node{ + Node{ + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + }, + ID: "1860451d-fb89-45b8-b54e-151afceb50e5", + LoadBalancerPool: struct { + ID string `mapstructure:"id"` + }{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + }, + Status: "ACTIVE", + UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC), + }, + Node{ + CreatedAt: time.Date(2014, 5, 31, 8, 23, 12, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + }{ + ID: "f28b870f-a063-498a-8b12-7025e5b1caa6", + }, + ID: "b70481dd-7edf-4dbb-a44b-41cc7679d4fb", + LoadBalancerPool: struct { + ID string `mapstructure:"id"` + }{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + }, + Status: "ADDING", + UpdatedAt: time.Date(2014, 5, 31, 8, 23, 26, 0, time.UTC), + }, + Node{ + CreatedAt: time.Date(2014, 5, 31, 8, 23, 18, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + }{ + ID: "a3d3a6b3-e4e4-496f-9a3d-5c987163e458", + }, + ID: "ced9ddc8-6fae-4e72-9457-16ead52b5515", + LoadBalancerPool: struct { + ID string `mapstructure:"id"` + }{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + }, + Status: "ADD_FAILED", + StatusDetail: "Unable to communicate with network device", + UpdatedAt: time.Date(2014, 5, 31, 8, 24, 36, 0, time.UTC), + }, + } + + count := 0 + err := ListNodes(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2").EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNodes(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateNode(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + } + } + `) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, ` + { + "created": "2014-05-30T03:23:42Z", + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + }, + "id": "1860451d-fb89-45b8-b54e-151afceb50e5", + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2" + }, + "status": "ACTIVE", + "status_detail": null, + "updated": "2014-05-30T03:24:18Z" + } + `) + }) + + expected := &Node{ + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + }, + ID: "1860451d-fb89-45b8-b54e-151afceb50e5", + LoadBalancerPool: struct { + ID string `mapstructure:"id"` + }{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + }, + Status: "ACTIVE", + UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC), + } + + actual, err := CreateNode(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", "d95ae0c4-6ab8-4873-b82f-f8433840cff2").Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expected, actual) +} + +func TestListNodesDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes/details", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, ` + [ + { + "created": "2014-05-30T03:23:42Z", + "cloud_server": { + "cloud_network": { + "cidr": "192.168.100.0/24", + "created": "2014-05-25T01:23:42Z", + "id": "07426958-1ebf-4c38-b032-d456820ca21a", + "name": "RC-CLOUD", + "private_ip_v4": "192.168.100.5", + "updated": "2014-05-25T02:28:44Z" + }, + "created": "2014-05-30T02:18:42Z", + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + "name": "RCv3TestServer1", + "updated": "2014-05-30T02:19:18Z" + }, + "id": "1860451d-fb89-45b8-b54e-151afceb50e5", + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + "name": "RCv3Test", + "node_counts": { + "cloud_servers": 3, + "external": 4, + "total": 7 + }, + "port": 80, + "status": "ACTIVE", + "status_detail": null, + "virtual_ip": "203.0.113.5" + }, + "status": "ACTIVE", + "status_detail": null, + "updated": "2014-05-30T03:24:18Z" + } + ] + `) + }) + + expected := []NodeDetails{ + NodeDetails{ + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + CloudNetwork struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + } `mapstructure:"cloud_network"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + CloudNetwork: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "07426958-1ebf-4c38-b032-d456820ca21a", + CIDR: "192.168.100.0/24", + CreatedAt: time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC), + Name: "RC-CLOUD", + PrivateIPv4: "192.168.100.5", + UpdatedAt: time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC), + }, + CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC), + Name: "RCv3TestServer1", + UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC), + }, + ID: "1860451d-fb89-45b8-b54e-151afceb50e5", + LoadBalancerPool: Pool{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + Name: "RCv3Test", + NodeCounts: struct { + CloudServers int `mapstructure:"cloud_servers"` + External int `mapstructure:"external"` + Total int `mapstructure:"total"` + }{ + CloudServers: 3, + External: 4, + Total: 7, + }, + Port: 80, + Status: "ACTIVE", + VirtualIP: "203.0.113.5", + }, + Status: "ACTIVE", + UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC), + }, + } + count := 0 + err := ListNodesDetails(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2").EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNodesDetails(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestGetNode(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes/1860451d-fb89-45b8-b54e-151afceb50e5", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "created": "2014-05-30T03:23:42Z", + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + }, + "id": "1860451d-fb89-45b8-b54e-151afceb50e5", + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2" + }, + "status": "ACTIVE", + "status_detail": null, + "updated": "2014-05-30T03:24:18Z" + } + `) + }) + + expected := &Node{ + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + }, + ID: "1860451d-fb89-45b8-b54e-151afceb50e5", + LoadBalancerPool: struct { + ID string `mapstructure:"id"` + }{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + }, + Status: "ACTIVE", + UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC), + } + + actual, err := GetNode(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", "1860451d-fb89-45b8-b54e-151afceb50e5").Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteNode(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes/1860451d-fb89-45b8-b54e-151afceb50e5", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + err := DeleteNode(client.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", "1860451d-fb89-45b8-b54e-151afceb50e5").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestGetNodeDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes/d95ae0c4-6ab8-4873-b82f-f8433840cff2/details", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "created": "2014-05-30T03:23:42Z", + "cloud_server": { + "cloud_network": { + "cidr": "192.168.100.0/24", + "created": "2014-05-25T01:23:42Z", + "id": "07426958-1ebf-4c38-b032-d456820ca21a", + "name": "RC-CLOUD", + "private_ip_v4": "192.168.100.5", + "updated": "2014-05-25T02:28:44Z" + }, + "created": "2014-05-30T02:18:42Z", + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + "name": "RCv3TestServer1", + "updated": "2014-05-30T02:19:18Z" + }, + "id": "1860451d-fb89-45b8-b54e-151afceb50e5", + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + "name": "RCv3Test", + "node_counts": { + "cloud_servers": 3, + "external": 4, + "total": 7 + }, + "port": 80, + "status": "ACTIVE", + "status_detail": null, + "virtual_ip": "203.0.113.5" + }, + "status": "ACTIVE", + "status_detail": null, + "updated": "2014-05-30T03:24:18Z" + } + `) + }) + + expected := &NodeDetails{ + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + CloudNetwork struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + } `mapstructure:"cloud_network"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + CloudNetwork: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "07426958-1ebf-4c38-b032-d456820ca21a", + CIDR: "192.168.100.0/24", + CreatedAt: time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC), + Name: "RC-CLOUD", + PrivateIPv4: "192.168.100.5", + UpdatedAt: time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC), + }, + CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC), + Name: "RCv3TestServer1", + UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC), + }, + ID: "1860451d-fb89-45b8-b54e-151afceb50e5", + LoadBalancerPool: Pool{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + Name: "RCv3Test", + NodeCounts: struct { + CloudServers int `mapstructure:"cloud_servers"` + External int `mapstructure:"external"` + Total int `mapstructure:"total"` + }{ + CloudServers: 3, + External: 4, + Total: 7, + }, + Port: 80, + Status: "ACTIVE", + VirtualIP: "203.0.113.5", + }, + Status: "ACTIVE", + UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC), + } + + actual, err := GetNodeDetails(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", "d95ae0c4-6ab8-4873-b82f-f8433840cff2").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} + +func TestCreateNodes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools/nodes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + [ + { + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + }, + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2" + } + }, + { + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + }, + "load_balancer_pool": { + "id": "33021100-4abf-4836-9080-465a6d87ab68" + } + } + ] + `) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, ` + [ + { + "created": "2014-05-30T03:23:42Z", + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + }, + "id": "1860451d-fb89-45b8-b54e-151afceb50e5", + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2" + }, + "status": "ADDING", + "status_detail": null, + "updated": null + }, + { + "created": "2014-05-31T08:23:12Z", + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + }, + "id": "b70481dd-7edf-4dbb-a44b-41cc7679d4fb", + "load_balancer_pool": { + "id": "33021100-4abf-4836-9080-465a6d87ab68" + }, + "status": "ADDING", + "status_detail": null, + "updated": null + } + ] + `) + }) + + expected := []Node{ + Node{ + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + }, + ID: "1860451d-fb89-45b8-b54e-151afceb50e5", + LoadBalancerPool: struct { + ID string `mapstructure:"id"` + }{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + }, + Status: "ADDING", + }, + Node{ + CreatedAt: time.Date(2014, 5, 31, 8, 23, 12, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + }, + ID: "b70481dd-7edf-4dbb-a44b-41cc7679d4fb", + LoadBalancerPool: struct { + ID string `mapstructure:"id"` + }{ + ID: "33021100-4abf-4836-9080-465a6d87ab68", + }, + Status: "ADDING", + }, + } + + opts := NodesOpts{ + NodeOpts{ + ServerID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + PoolID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + }, + NodeOpts{ + ServerID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + PoolID: "33021100-4abf-4836-9080-465a6d87ab68", + }, + } + actual, err := CreateNodes(fake.ServiceClient(), opts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteNodes(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools/nodes", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + [ + { + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + }, + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2" + } + }, + { + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + }, + "load_balancer_pool": { + "id": "33021100-4abf-4836-9080-465a6d87ab68" + } + } + ] + `) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + opts := NodesOpts{ + NodeOpts{ + ServerID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + PoolID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + }, + NodeOpts{ + ServerID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + PoolID: "33021100-4abf-4836-9080-465a6d87ab68", + }, + } + err := DeleteNodes(client.ServiceClient(), opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestListNodesForServerDetails(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/load_balancer_pools/nodes/details", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, ` + [ + { + "created": "2014-05-30T03:23:42Z", + "id": "1860451d-fb89-45b8-b54e-151afceb50e5", + "load_balancer_pool": { + "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + "name": "RCv3Test", + "node_counts": { + "cloud_servers": 3, + "external": 4, + "total": 7 + }, + "port": 80, + "status": "ACTIVE", + "status_detail": null, + "virtual_ip": "203.0.113.5" + }, + "status": "ACTIVE", + "status_detail": null, + "updated": "2014-05-30T03:24:18Z" + } + ] + `) + }) + + expected := []NodeDetailsForServer{ + NodeDetailsForServer{ + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + ID: "1860451d-fb89-45b8-b54e-151afceb50e5", + LoadBalancerPool: Pool{ + ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", + Name: "RCv3Test", + NodeCounts: struct { + CloudServers int `mapstructure:"cloud_servers"` + External int `mapstructure:"external"` + Total int `mapstructure:"total"` + }{ + CloudServers: 3, + External: 4, + Total: 7, + }, + Port: 80, + Status: "ACTIVE", + VirtualIP: "203.0.113.5", + }, + Status: "ACTIVE", + UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC), + }, + } + count := 0 + err := ListNodesDetailsForServer(fake.ServiceClient(), "07426958-1ebf-4c38-b032-d456820ca21a").EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractNodesDetailsForServer(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/results.go new file mode 100644 index 000000000000..e5e914b1e2bd --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/results.go @@ -0,0 +1,505 @@ +package lbpools + +import ( + "fmt" + "reflect" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Pool represents a load balancer pool associated with a RackConnect configuration. +type Pool struct { + // The unique ID of the load balancer pool. + ID string `mapstructure:"id"` + // The name of the load balancer pool. + Name string `mapstructure:"name"` + // The node counts associated witht the load balancer pool. + NodeCounts struct { + // The number of nodes associated with this LB pool for this account. + CloudServers int `mapstructure:"cloud_servers"` + // The number of nodes associated with this LB pool from other accounts. + External int `mapstructure:"external"` + // The total number of nodes associated with this LB pool. + Total int `mapstructure:"total"` + } `mapstructure:"node_counts"` + // The port of the LB pool + Port int `mapstructure:"port"` + // The status of the LB pool + Status string `mapstructure:"status"` + // The details of the status of the LB pool + StatusDetail string `mapstructure:"status_detail"` + // The virtual IP of the LB pool + VirtualIP string `mapstructure:"virtual_ip"` +} + +// PoolPage is the page returned by a pager when traversing over a +// collection of Pools. +type PoolPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a PoolPage contains no Pools. +func (r PoolPage) IsEmpty() (bool, error) { + cns, err := ExtractPools(r) + if err != nil { + return true, err + } + return len(cns) == 0, nil +} + +// ExtractPools extracts and returns Pools. It is used while iterating over +// an lbpools.List call. +func ExtractPools(page pagination.Page) ([]Pool, error) { + var res []Pool + err := mapstructure.Decode(page.(PoolPage).Body, &res) + return res, err +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + gophercloud.Result +} + +// Extract is a function that extracts an LBPool from a GetResult. +func (r GetResult) Extract() (*Pool, error) { + if r.Err != nil { + return nil, r.Err + } + var res Pool + err := mapstructure.Decode(r.Body, &res) + return &res, err +} + +// Node represents a load balancer pool node associated with a RackConnect configuration. +type Node struct { + // The unique ID of the LB node. + ID string `mapstructure:"id"` + // The cloud server (node) of the load balancer pool. + CloudServer struct { + // The cloud server ID. + ID string `mapstructure:"id"` + } `mapstructure:"cloud_server"` + // The load balancer pool. + LoadBalancerPool struct { + // The LB pool ID. + ID string `mapstructure:"id"` + } `mapstructure:"load_balancer_pool"` + // The status of the LB pool. + Status string `mapstructure:"status"` + // The details of the status of the LB pool. + StatusDetail string `mapstructure:"status_detail"` + // The time the LB node was created. + CreatedAt time.Time `mapstructure:"-"` + // The time the LB node was last updated. + UpdatedAt time.Time `mapstructure:"-"` +} + +// NodePage is the page returned by a pager when traversing over a +// collection of Nodes. +type NodePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a NodePage contains no Nodes. +func (r NodePage) IsEmpty() (bool, error) { + n, err := ExtractNodes(r) + if err != nil { + return true, err + } + return len(n) == 0, nil +} + +// ExtractNodes extracts and returns a slice of Nodes. It is used while iterating over +// an lbpools.ListNodes call. +func ExtractNodes(page pagination.Page) ([]Node, error) { + var res []Node + casted := page.(NodePage).Body + err := mapstructure.Decode(casted, &res) + + var rawNodes []interface{} + switch casted.(type) { + case interface{}: + rawNodes = casted.([]interface{}) + default: + return res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted)) + } + + for i := range rawNodes { + thisNode := (rawNodes[i]).(map[string]interface{}) + + if t, ok := thisNode["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CreatedAt = creationTime + } + + if t, ok := thisNode["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].UpdatedAt = updatedTime + } + } + + return res, err +} + +// NodeResult represents a result that can be extracted as a Node. +type NodeResult struct { + gophercloud.Result +} + +// CreateNodeResult represents the result of an CreateNode operation. +type CreateNodeResult struct { + NodeResult +} + +// GetNodeResult represents the result of an GetNode operation. +type GetNodeResult struct { + NodeResult +} + +// Extract is a function that extracts a Node from a NodeResult. +func (r NodeResult) Extract() (*Node, error) { + if r.Err != nil { + return nil, r.Err + } + var res Node + err := mapstructure.Decode(r.Body, &res) + + b := r.Body.(map[string]interface{}) + + if date, ok := b["created"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.CreatedAt = t + } + + if date, ok := b["updated"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.UpdatedAt = t + } + + return &res, err +} + +// NodeDetails represents a load balancer pool node associated with a RackConnect configuration +// with all its details. +type NodeDetails struct { + // The unique ID of the LB node. + ID string `mapstructure:"id"` + // The cloud server (node) of the load balancer pool. + CloudServer struct { + // The cloud server ID. + ID string `mapstructure:"id"` + // The name of the server. + Name string `mapstructure:"name"` + // The cloud network for the cloud server. + CloudNetwork struct { + // The network ID. + ID string `mapstructure:"id"` + // The network name. + Name string `mapstructure:"name"` + // The network's private IPv4 address. + PrivateIPv4 string `mapstructure:"private_ip_v4"` + // The IP range for the network. + CIDR string `mapstructure:"cidr"` + // The datetime the network was created. + CreatedAt time.Time `mapstructure:"-"` + // The last datetime the network was updated. + UpdatedAt time.Time `mapstructure:"-"` + } `mapstructure:"cloud_network"` + // The datetime the server was created. + CreatedAt time.Time `mapstructure:"-"` + // The datetime the server was last updated. + UpdatedAt time.Time `mapstructure:"-"` + } `mapstructure:"cloud_server"` + // The load balancer pool. + LoadBalancerPool Pool `mapstructure:"load_balancer_pool"` + // The status of the LB pool. + Status string `mapstructure:"status"` + // The details of the status of the LB pool. + StatusDetail string `mapstructure:"status_detail"` + // The time the LB node was created. + CreatedAt time.Time `mapstructure:"-"` + // The time the LB node was last updated. + UpdatedAt time.Time `mapstructure:"-"` +} + +// NodeDetailsPage is the page returned by a pager when traversing over a +// collection of NodeDetails. +type NodeDetailsPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a NodeDetailsPage contains no NodeDetails. +func (r NodeDetailsPage) IsEmpty() (bool, error) { + n, err := ExtractNodesDetails(r) + if err != nil { + return true, err + } + return len(n) == 0, nil +} + +// ExtractNodesDetails extracts and returns a slice of NodeDetails. It is used while iterating over +// an lbpools.ListNodesDetails call. +func ExtractNodesDetails(page pagination.Page) ([]NodeDetails, error) { + var res []NodeDetails + casted := page.(NodeDetailsPage).Body + err := mapstructure.Decode(casted, &res) + + var rawNodesDetails []interface{} + switch casted.(type) { + case interface{}: + rawNodesDetails = casted.([]interface{}) + default: + return res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted)) + } + + for i := range rawNodesDetails { + thisNodeDetails := (rawNodesDetails[i]).(map[string]interface{}) + + if t, ok := thisNodeDetails["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CreatedAt = creationTime + } + + if t, ok := thisNodeDetails["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].UpdatedAt = updatedTime + } + + if cs, ok := thisNodeDetails["cloud_server"].(map[string]interface{}); ok { + if t, ok := cs["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CloudServer.CreatedAt = creationTime + } + if t, ok := cs["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CloudServer.UpdatedAt = updatedTime + } + if cn, ok := cs["cloud_network"].(map[string]interface{}); ok { + if t, ok := cn["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CloudServer.CloudNetwork.CreatedAt = creationTime + } + if t, ok := cn["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CloudServer.CloudNetwork.UpdatedAt = updatedTime + } + } + } + } + + return res, err +} + +// GetNodeDetailsResult represents the result of an NodeDetails operation. +type GetNodeDetailsResult struct { + gophercloud.Result +} + +// Extract is a function that extracts a NodeDetails from a NodeDetailsResult. +func (r GetNodeDetailsResult) Extract() (*NodeDetails, error) { + if r.Err != nil { + return nil, r.Err + } + var res NodeDetails + err := mapstructure.Decode(r.Body, &res) + + b := r.Body.(map[string]interface{}) + + if date, ok := b["created"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.CreatedAt = t + } + + if date, ok := b["updated"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.UpdatedAt = t + } + + if cs, ok := b["cloud_server"].(map[string]interface{}); ok { + if t, ok := cs["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return &res, err + } + res.CloudServer.CreatedAt = creationTime + } + if t, ok := cs["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return &res, err + } + res.CloudServer.UpdatedAt = updatedTime + } + if cn, ok := cs["cloud_network"].(map[string]interface{}); ok { + if t, ok := cn["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return &res, err + } + res.CloudServer.CloudNetwork.CreatedAt = creationTime + } + if t, ok := cn["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return &res, err + } + res.CloudServer.CloudNetwork.UpdatedAt = updatedTime + } + } + } + + return &res, err +} + +// DeleteNodeResult represents the result of a DeleteNode operation. +type DeleteNodeResult struct { + gophercloud.ErrResult +} + +// CreateNodesResult represents the result of a CreateNodes operation. +type CreateNodesResult struct { + gophercloud.Result +} + +// Extract is a function that extracts a slice of Nodes from a CreateNodesResult. +func (r CreateNodesResult) Extract() ([]Node, error) { + if r.Err != nil { + return nil, r.Err + } + var res []Node + err := mapstructure.Decode(r.Body, &res) + + b := r.Body.([]interface{}) + for i := range b { + if date, ok := b[i].(map[string]interface{})["created"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res[i].CreatedAt = t + } + if date, ok := b[i].(map[string]interface{})["updated"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res[i].UpdatedAt = t + } + } + + return res, err +} + +// DeleteNodesResult represents the result of a DeleteNodes operation. +type DeleteNodesResult struct { + gophercloud.ErrResult +} + +// NodeDetailsForServer represents a load balancer pool node associated with a RackConnect configuration +// with all its details for a particular server. +type NodeDetailsForServer struct { + // The unique ID of the LB node. + ID string `mapstructure:"id"` + // The load balancer pool. + LoadBalancerPool Pool `mapstructure:"load_balancer_pool"` + // The status of the LB pool. + Status string `mapstructure:"status"` + // The details of the status of the LB pool. + StatusDetail string `mapstructure:"status_detail"` + // The time the LB node was created. + CreatedAt time.Time `mapstructure:"-"` + // The time the LB node was last updated. + UpdatedAt time.Time `mapstructure:"-"` +} + +// NodeDetailsForServerPage is the page returned by a pager when traversing over a +// collection of NodeDetailsForServer. +type NodeDetailsForServerPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a NodeDetailsForServerPage contains no NodeDetailsForServer. +func (r NodeDetailsForServerPage) IsEmpty() (bool, error) { + n, err := ExtractNodesDetailsForServer(r) + if err != nil { + return true, err + } + return len(n) == 0, nil +} + +// ExtractNodesDetailsForServer extracts and returns a slice of NodeDetailsForServer. It is used while iterating over +// an lbpools.ListNodesDetailsForServer call. +func ExtractNodesDetailsForServer(page pagination.Page) ([]NodeDetailsForServer, error) { + var res []NodeDetailsForServer + casted := page.(NodeDetailsForServerPage).Body + err := mapstructure.Decode(casted, &res) + + var rawNodesDetails []interface{} + switch casted.(type) { + case interface{}: + rawNodesDetails = casted.([]interface{}) + default: + return res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted)) + } + + for i := range rawNodesDetails { + thisNodeDetails := (rawNodesDetails[i]).(map[string]interface{}) + + if t, ok := thisNodeDetails["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CreatedAt = creationTime + } + + if t, ok := thisNodeDetails["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].UpdatedAt = updatedTime + } + } + + return res, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/urls.go new file mode 100644 index 000000000000..c238239f61e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools/urls.go @@ -0,0 +1,49 @@ +package lbpools + +import "github.com/rackspace/gophercloud" + +var root = "load_balancer_pools" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(root) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(root, id) +} + +func listNodesURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(root, id, "nodes") +} + +func createNodeURL(c *gophercloud.ServiceClient, id string) string { + return listNodesURL(c, id) +} + +func listNodesDetailsURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(root, id, "nodes", "details") +} + +func nodeURL(c *gophercloud.ServiceClient, poolID, nodeID string) string { + return c.ServiceURL(root, poolID, "nodes", nodeID) +} + +func deleteNodeURL(c *gophercloud.ServiceClient, poolID, nodeID string) string { + return nodeURL(c, poolID, nodeID) +} + +func nodeDetailsURL(c *gophercloud.ServiceClient, poolID, nodeID string) string { + return c.ServiceURL(root, poolID, "nodes", nodeID, "details") +} + +func createNodesURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(root, "nodes") +} + +func deleteNodesURL(c *gophercloud.ServiceClient) string { + return createNodesURL(c) +} + +func listNodesForServerURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL(root, "nodes", "details?cloud_server_id="+serverID) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/requests.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/requests.go new file mode 100644 index 000000000000..1164260109e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/requests.go @@ -0,0 +1,50 @@ +package publicips + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// List returns all public IPs. +func List(c *gophercloud.ServiceClient) pagination.Pager { + url := listURL(c) + createPage := func(r pagination.PageResult) pagination.Page { + return PublicIPPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} + +// Create adds a public IP to the server with the given serverID. +func Create(c *gophercloud.ServiceClient, serverID string) CreateResult { + var res CreateResult + reqBody := map[string]interface{}{ + "cloud_server": map[string]string{ + "id": serverID, + }, + } + _, res.Err = c.Post(createURL(c), reqBody, &res.Body, nil) + return res +} + +// ListForServer returns all public IPs for the server with the given serverID. +func ListForServer(c *gophercloud.ServiceClient, serverID string) pagination.Pager { + url := listForServerURL(c, serverID) + createPage := func(r pagination.PageResult) pagination.Page { + return PublicIPPage{pagination.SinglePageBase(r)} + } + return pagination.NewPager(c, url, createPage) +} + +// Get retrieves the public IP with the given id. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(getURL(c, id), &res.Body, nil) + return res +} + +// Delete removes the public IP with the given id. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(deleteURL(c, id), nil) + return res +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/requests_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/requests_test.go new file mode 100644 index 000000000000..61da2b03d9be --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/requests_test.go @@ -0,0 +1,378 @@ +package publicips + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" + fake "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListIPs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/public_ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `[ + { + "created": "2014-05-30T03:23:42Z", + "cloud_server": { + "cloud_network": { + "cidr": "192.168.100.0/24", + "created": "2014-05-25T01:23:42Z", + "id": "07426958-1ebf-4c38-b032-d456820ca21a", + "name": "RC-CLOUD", + "private_ip_v4": "192.168.100.5", + "updated": "2014-05-25T02:28:44Z" + }, + "created": "2014-05-30T02:18:42Z", + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + "name": "RCv3TestServer1", + "updated": "2014-05-30T02:19:18Z" + }, + "id": "2d0f586b-37a7-4ae0-adac-2743d5feb450", + "public_ip_v4": "203.0.113.110", + "status": "ACTIVE", + "status_detail": null, + "updated": "2014-05-30T03:24:18Z" + } + ]`) + }) + + expected := []PublicIP{ + PublicIP{ + ID: "2d0f586b-37a7-4ae0-adac-2743d5feb450", + PublicIPv4: "203.0.113.110", + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + CloudNetwork struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + } `mapstructure:"cloud_network"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + CloudNetwork: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "07426958-1ebf-4c38-b032-d456820ca21a", + CIDR: "192.168.100.0/24", + CreatedAt: time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC), + Name: "RC-CLOUD", + PrivateIPv4: "192.168.100.5", + UpdatedAt: time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC), + }, + CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC), + Name: "RCv3TestServer1", + UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC), + }, + Status: "ACTIVE", + UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC), + }, + } + + count := 0 + err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractPublicIPs(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestCreateIP(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/public_ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "cloud_server": { + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2" + } + } + `) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, ` + { + "created": "2014-05-30T03:23:42Z", + "cloud_server": { + "cloud_network": { + "cidr": "192.168.100.0/24", + "created": "2014-05-25T01:23:42Z", + "id": "07426958-1ebf-4c38-b032-d456820ca21a", + "name": "RC-CLOUD", + "private_ip_v4": "192.168.100.5", + "updated": "2014-05-25T02:28:44Z" + }, + "created": "2014-05-30T02:18:42Z", + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + "name": "RCv3TestServer1", + "updated": "2014-05-30T02:19:18Z" + }, + "id": "2d0f586b-37a7-4ae0-adac-2743d5feb450", + "status": "ADDING" + }`) + }) + + expected := &PublicIP{ + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + CloudNetwork struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + } `mapstructure:"cloud_network"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + CloudNetwork: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "07426958-1ebf-4c38-b032-d456820ca21a", + CIDR: "192.168.100.0/24", + CreatedAt: time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC), + Name: "RC-CLOUD", + PrivateIPv4: "192.168.100.5", + UpdatedAt: time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC), + }, + CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC), + Name: "RCv3TestServer1", + UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC), + }, + ID: "2d0f586b-37a7-4ae0-adac-2743d5feb450", + Status: "ADDING", + } + + actual, err := Create(fake.ServiceClient(), "d95ae0c4-6ab8-4873-b82f-f8433840cff2").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestGetIP(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/public_ips/2d0f586b-37a7-4ae0-adac-2743d5feb450", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` + { + "created": "2014-05-30T03:23:42Z", + "cloud_server": { + "cloud_network": { + "cidr": "192.168.100.0/24", + "created": "2014-05-25T01:23:42Z", + "id": "07426958-1ebf-4c38-b032-d456820ca21a", + "name": "RC-CLOUD", + "private_ip_v4": "192.168.100.5", + "updated": "2014-05-25T02:28:44Z" + }, + "created": "2014-05-30T02:18:42Z", + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + "name": "RCv3TestServer1", + "updated": "2014-05-30T02:19:18Z" + }, + "id": "2d0f586b-37a7-4ae0-adac-2743d5feb450", + "public_ip_v4": "203.0.113.110", + "status": "ACTIVE", + "status_detail": null, + "updated": "2014-05-30T03:24:18Z" + }`) + }) + + expected := &PublicIP{ + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + CloudNetwork struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + } `mapstructure:"cloud_network"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + CloudNetwork: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "07426958-1ebf-4c38-b032-d456820ca21a", + CIDR: "192.168.100.0/24", + CreatedAt: time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC), + Name: "RC-CLOUD", + PrivateIPv4: "192.168.100.5", + UpdatedAt: time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC), + }, + CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC), + Name: "RCv3TestServer1", + UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC), + }, + ID: "2d0f586b-37a7-4ae0-adac-2743d5feb450", + Status: "ACTIVE", + PublicIPv4: "203.0.113.110", + UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC), + } + + actual, err := Get(fake.ServiceClient(), "2d0f586b-37a7-4ae0-adac-2743d5feb450").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteIP(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/public_ips/2d0f586b-37a7-4ae0-adac-2743d5feb450", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + err := Delete(client.ServiceClient(), "2d0f586b-37a7-4ae0-adac-2743d5feb450").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestListForServer(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/public_ips", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, ` + [ + { + "created": "2014-05-30T03:23:42Z", + "cloud_server": { + "cloud_network": { + "cidr": "192.168.100.0/24", + "created": "2014-05-25T01:23:42Z", + "id": "07426958-1ebf-4c38-b032-d456820ca21a", + "name": "RC-CLOUD", + "private_ip_v4": "192.168.100.5", + "updated": "2014-05-25T02:28:44Z" + }, + "created": "2014-05-30T02:18:42Z", + "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + "name": "RCv3TestServer1", + "updated": "2014-05-30T02:19:18Z" + }, + "id": "2d0f586b-37a7-4ae0-adac-2743d5feb450", + "public_ip_v4": "203.0.113.110", + "status": "ACTIVE", + "updated": "2014-05-30T03:24:18Z" + } + ]`) + }) + + expected := []PublicIP{ + PublicIP{ + CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC), + CloudServer: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + CloudNetwork struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + } `mapstructure:"cloud_network"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2", + CloudNetwork: struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` + PrivateIPv4 string `mapstructure:"private_ip_v4"` + CIDR string `mapstructure:"cidr"` + CreatedAt time.Time `mapstructure:"-"` + UpdatedAt time.Time `mapstructure:"-"` + }{ + ID: "07426958-1ebf-4c38-b032-d456820ca21a", + CIDR: "192.168.100.0/24", + CreatedAt: time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC), + Name: "RC-CLOUD", + PrivateIPv4: "192.168.100.5", + UpdatedAt: time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC), + }, + CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC), + Name: "RCv3TestServer1", + UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC), + }, + ID: "2d0f586b-37a7-4ae0-adac-2743d5feb450", + Status: "ACTIVE", + PublicIPv4: "203.0.113.110", + UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC), + }, + } + count := 0 + err := ListForServer(fake.ServiceClient(), "d95ae0c4-6ab8-4873-b82f-f8433840cff2").EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractPublicIPs(page) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/results.go new file mode 100644 index 000000000000..132cf770a0fc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/results.go @@ -0,0 +1,221 @@ +package publicips + +import ( + "fmt" + "reflect" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// PublicIP represents a public IP address. +type PublicIP struct { + // The unique ID of the public IP. + ID string `mapstructure:"id"` + // The IPv4 address of the public IP. + PublicIPv4 string `mapstructure:"public_ip_v4"` + // The cloud server (node) of the public IP. + CloudServer struct { + // The cloud server ID. + ID string `mapstructure:"id"` + // The name of the server. + Name string `mapstructure:"name"` + // The cloud network for the cloud server. + CloudNetwork struct { + // The network ID. + ID string `mapstructure:"id"` + // The network name. + Name string `mapstructure:"name"` + // The network's private IPv4 address. + PrivateIPv4 string `mapstructure:"private_ip_v4"` + // The IP range for the network. + CIDR string `mapstructure:"cidr"` + // The datetime the network was created. + CreatedAt time.Time `mapstructure:"-"` + // The last datetime the network was updated. + UpdatedAt time.Time `mapstructure:"-"` + } `mapstructure:"cloud_network"` + // The datetime the server was created. + CreatedAt time.Time `mapstructure:"-"` + // The datetime the server was last updated. + UpdatedAt time.Time `mapstructure:"-"` + } `mapstructure:"cloud_server"` + // The status of the public IP. + Status string `mapstructure:"status"` + // The details of the status of the public IP. + StatusDetail string `mapstructure:"status_detail"` + // The time the public IP was created. + CreatedAt time.Time `mapstructure:"-"` + // The time the public IP was last updated. + UpdatedAt time.Time `mapstructure:"-"` +} + +// PublicIPPage is the page returned by a pager when traversing over a +// collection of PublicIPs. +type PublicIPPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a PublicIPPage contains no PublicIPs. +func (r PublicIPPage) IsEmpty() (bool, error) { + n, err := ExtractPublicIPs(r) + if err != nil { + return true, err + } + return len(n) == 0, nil +} + +// ExtractPublicIPs extracts and returns a slice of PublicIPs. It is used while iterating over +// a publicips.List call. +func ExtractPublicIPs(page pagination.Page) ([]PublicIP, error) { + var res []PublicIP + casted := page.(PublicIPPage).Body + err := mapstructure.Decode(casted, &res) + + var rawNodesDetails []interface{} + switch casted.(type) { + case interface{}: + rawNodesDetails = casted.([]interface{}) + default: + return res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted)) + } + + for i := range rawNodesDetails { + thisNodeDetails := (rawNodesDetails[i]).(map[string]interface{}) + + if t, ok := thisNodeDetails["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CreatedAt = creationTime + } + + if t, ok := thisNodeDetails["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].UpdatedAt = updatedTime + } + + if cs, ok := thisNodeDetails["cloud_server"].(map[string]interface{}); ok { + if t, ok := cs["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CloudServer.CreatedAt = creationTime + } + if t, ok := cs["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CloudServer.UpdatedAt = updatedTime + } + if cn, ok := cs["cloud_network"].(map[string]interface{}); ok { + if t, ok := cn["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CloudServer.CloudNetwork.CreatedAt = creationTime + } + if t, ok := cn["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return res, err + } + res[i].CloudServer.CloudNetwork.UpdatedAt = updatedTime + } + } + } + } + + return res, err +} + +// PublicIPResult represents a result that can be extracted into a PublicIP. +type PublicIPResult struct { + gophercloud.Result +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + PublicIPResult +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + PublicIPResult +} + +// Extract is a function that extracts a PublicIP from a PublicIPResult. +func (r PublicIPResult) Extract() (*PublicIP, error) { + if r.Err != nil { + return nil, r.Err + } + var res PublicIP + err := mapstructure.Decode(r.Body, &res) + + b := r.Body.(map[string]interface{}) + + if date, ok := b["created"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.CreatedAt = t + } + + if date, ok := b["updated"]; ok && date != nil { + t, err := time.Parse(time.RFC3339, date.(string)) + if err != nil { + return nil, err + } + res.UpdatedAt = t + } + + if cs, ok := b["cloud_server"].(map[string]interface{}); ok { + if t, ok := cs["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return &res, err + } + res.CloudServer.CreatedAt = creationTime + } + if t, ok := cs["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return &res, err + } + res.CloudServer.UpdatedAt = updatedTime + } + if cn, ok := cs["cloud_network"].(map[string]interface{}); ok { + if t, ok := cn["created"].(string); ok && t != "" { + creationTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return &res, err + } + res.CloudServer.CloudNetwork.CreatedAt = creationTime + } + if t, ok := cn["updated"].(string); ok && t != "" { + updatedTime, err := time.Parse(time.RFC3339, t) + if err != nil { + return &res, err + } + res.CloudServer.CloudNetwork.UpdatedAt = updatedTime + } + } + } + + return &res, err +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/urls.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/urls.go new file mode 100644 index 000000000000..6f310be4e816 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips/urls.go @@ -0,0 +1,25 @@ +package publicips + +import "github.com/rackspace/gophercloud" + +var root = "public_ips" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(root) +} + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(root) +} + +func listForServerURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL(root + "?cloud_server_id=" + serverID) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(root, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go new file mode 100644 index 000000000000..7c86ce46230c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/results.go @@ -0,0 +1,150 @@ +package gophercloud + +import ( + "encoding/json" + "net/http" + "reflect" + + "github.com/mitchellh/mapstructure" +) + +/* +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 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 the Extract call. + Err error +} + +// 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 { + panic(err.Error()) + } + return string(pretty) +} + +// 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, or nil, from a result. +func (r ErrResult) ExtractErr() error { + return r.Err +} + +/* +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. +// +// header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader() +func (hr HeaderResult) ExtractHeader() (http.Header, error) { + return hr.Header, hr.Err +} + +// 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 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 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 + + for _, l := range links { + if l.Rel == "next" { + url = l.Href + } + } + + if url == "" { + return "", nil + } + + return url, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/acceptancetest b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/acceptancetest new file mode 100644 index 000000000000..f9c89f4dfdd3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/acceptancetest @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run the acceptance tests. + +exec go test -p=1 -tags 'acceptance fixtures' github.com/rackspace/gophercloud/acceptance/... $@ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/bootstrap b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/bootstrap new file mode 100644 index 000000000000..6bae6e8f14f3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/bootstrap @@ -0,0 +1,26 @@ +#!/bin/bash +# +# This script helps new contributors set up their local workstation for +# gophercloud development and contributions. + +# Create the environment +export GOPATH=$HOME/go/gophercloud +mkdir -p $GOPATH + +# Download gophercloud into that environment +go get github.com/rackspace/gophercloud +cd $GOPATH/src/github.com/rackspace/gophercloud +git checkout master + +# Write out the env.sh convenience file. +cd $GOPATH +cat <env.sh +#!/bin/bash +export GOPATH=$(pwd) +export GOPHERCLOUD=$GOPATH/src/github.com/rackspace/gophercloud +EOF +chmod a+x env.sh + +# Make changes immediately available as a convenience. +. ./env.sh + diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/cibuild b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/cibuild new file mode 100644 index 000000000000..1cb389e7dce0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/cibuild @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Test script to be invoked by Travis. + +exec script/unittest -v diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/test b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/test new file mode 100644 index 000000000000..1e03dff8ab34 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/test @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run all the tests. + +exec go test -tags 'acceptance fixtures' ./... $@ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/unittest b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/unittest new file mode 100644 index 000000000000..d3440a902c01 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/script/unittest @@ -0,0 +1,5 @@ +#!/bin/bash +# +# Run the unit tests. + +exec go test -tags fixtures ./... $@ diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client.go new file mode 100644 index 000000000000..3490da05f272 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client.go @@ -0,0 +1,32 @@ +package gophercloud + +import "strings" + +// ServiceClient stores details required to interact with a specific service API implemented by a provider. +// Generally, you'll acquire these by calling the appropriate `New` method on a ProviderClient. +type ServiceClient struct { + // ProviderClient is a reference to the provider that implements this service. + *ProviderClient + + // Endpoint is the base URL of the service's API, acquired from a service catalog. + // It MUST end with a /. + Endpoint string + + // ResourceBase is the base URL shared by the resources within a service's API. It should include + // the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used + // as-is, instead. + ResourceBase string +} + +// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /. +func (client *ServiceClient) ResourceBaseURL() string { + if client.ResourceBase != "" { + return client.ResourceBase + } + return client.Endpoint +} + +// ServiceURL constructs a URL for a resource belonging to this provider. +func (client *ServiceClient) ServiceURL(parts ...string) string { + return client.ResourceBaseURL() + strings.Join(parts, "/") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client_test.go new file mode 100644 index 000000000000..84beb3f7681a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_client_test.go @@ -0,0 +1,14 @@ +package gophercloud + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestServiceURL(t *testing.T) { + c := &ServiceClient{Endpoint: "http://123.45.67.8/"} + expected := "http://123.45.67.8/more/parts/here" + actual := c.ServiceURL("more", "parts", "here") + th.CheckEquals(t, expected, actual) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/client/fake.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/client/fake.go new file mode 100644 index 000000000000..5b69b058f1fe --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/client/fake.go @@ -0,0 +1,17 @@ +package client + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/testhelper" +) + +// Fake token to use. +const TokenID = "cbc36478b0bd8e67e89469c7749d4127" + +// ServiceClient returns a generic service client for use in tests. +func ServiceClient() *gophercloud.ServiceClient { + return &gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{TokenID: TokenID}, + Endpoint: testhelper.Endpoint(), + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/convenience.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/convenience.go new file mode 100644 index 000000000000..cf33e1ad1a6b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/convenience.go @@ -0,0 +1,329 @@ +package testhelper + +import ( + "encoding/json" + "fmt" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" +) + +const ( + logBodyFmt = "\033[1;31m%s %s\033[0m" + greenCode = "\033[0m\033[1;32m" + yellowCode = "\033[0m\033[1;33m" + resetCode = "\033[0m\033[1;31m" +) + +func prefix(depth int) string { + _, file, line, _ := runtime.Caller(depth) + return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line) +} + +func green(str interface{}) string { + return fmt.Sprintf("%s%#v%s", greenCode, str, resetCode) +} + +func yellow(str interface{}) string { + return fmt.Sprintf("%s%#v%s", yellowCode, str, resetCode) +} + +func logFatal(t *testing.T, str string) { + t.Fatalf(logBodyFmt, prefix(3), str) +} + +func logError(t *testing.T, str string) { + t.Errorf(logBodyFmt, prefix(3), str) +} + +type diffLogger func([]string, interface{}, interface{}) + +type visit struct { + a1 uintptr + a2 uintptr + typ reflect.Type +} + +// Recursively visits the structures of "expected" and "actual". The diffLogger function will be +// invoked with each different value encountered, including the reference path that was followed +// to get there. +func deepDiffEqual(expected, actual reflect.Value, visited map[visit]bool, path []string, logDifference diffLogger) { + defer func() { + // Fall back to the regular reflect.DeepEquals function. + if r := recover(); r != nil { + var e, a interface{} + if expected.IsValid() { + e = expected.Interface() + } + if actual.IsValid() { + a = actual.Interface() + } + + if !reflect.DeepEqual(e, a) { + logDifference(path, e, a) + } + } + }() + + if !expected.IsValid() && actual.IsValid() { + logDifference(path, nil, actual.Interface()) + return + } + if expected.IsValid() && !actual.IsValid() { + logDifference(path, expected.Interface(), nil) + return + } + if !expected.IsValid() && !actual.IsValid() { + return + } + + hard := func(k reflect.Kind) bool { + switch k { + case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct: + return true + } + return false + } + + if expected.CanAddr() && actual.CanAddr() && hard(expected.Kind()) { + addr1 := expected.UnsafeAddr() + addr2 := actual.UnsafeAddr() + + if addr1 > addr2 { + addr1, addr2 = addr2, addr1 + } + + if addr1 == addr2 { + // References are identical. We can short-circuit + return + } + + typ := expected.Type() + v := visit{addr1, addr2, typ} + if visited[v] { + // Already visited. + return + } + + // Remember this visit for later. + visited[v] = true + } + + switch expected.Kind() { + case reflect.Array: + for i := 0; i < expected.Len(); i++ { + hop := append(path, fmt.Sprintf("[%d]", i)) + deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference) + } + return + case reflect.Slice: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() { + return + } + for i := 0; i < expected.Len(); i++ { + hop := append(path, fmt.Sprintf("[%d]", i)) + deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference) + } + return + case reflect.Interface: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference) + return + case reflect.Ptr: + deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference) + return + case reflect.Struct: + for i, n := 0, expected.NumField(); i < n; i++ { + field := expected.Type().Field(i) + hop := append(path, "."+field.Name) + deepDiffEqual(expected.Field(i), actual.Field(i), visited, hop, logDifference) + } + return + case reflect.Map: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + return + } + if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() { + return + } + + var keys []reflect.Value + if expected.Len() >= actual.Len() { + keys = expected.MapKeys() + } else { + keys = actual.MapKeys() + } + + for _, k := range keys { + expectedValue := expected.MapIndex(k) + actualValue := expected.MapIndex(k) + + if !expectedValue.IsValid() { + logDifference(path, nil, actual.Interface()) + return + } + if !actualValue.IsValid() { + logDifference(path, expected.Interface(), nil) + return + } + + hop := append(path, fmt.Sprintf("[%v]", k)) + deepDiffEqual(expectedValue, actualValue, visited, hop, logDifference) + } + return + case reflect.Func: + if expected.IsNil() != actual.IsNil() { + logDifference(path, expected.Interface(), actual.Interface()) + } + return + default: + if expected.Interface() != actual.Interface() { + logDifference(path, expected.Interface(), actual.Interface()) + } + } +} + +func deepDiff(expected, actual interface{}, logDifference diffLogger) { + if expected == nil || actual == nil { + logDifference([]string{}, expected, actual) + return + } + + expectedValue := reflect.ValueOf(expected) + actualValue := reflect.ValueOf(actual) + + if expectedValue.Type() != actualValue.Type() { + logDifference([]string{}, expected, actual) + return + } + deepDiffEqual(expectedValue, actualValue, map[visit]bool{}, []string{}, logDifference) +} + +// AssertEquals compares two arbitrary values and performs a comparison. If the +// comparison fails, a fatal error is raised that will fail the test +func AssertEquals(t *testing.T, expected, actual interface{}) { + if expected != actual { + logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual))) + } +} + +// CheckEquals is similar to AssertEquals, except with a non-fatal error +func CheckEquals(t *testing.T, expected, actual interface{}) { + if expected != actual { + logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual))) + } +} + +// AssertDeepEquals - like Equals - performs a comparison - but on more complex +// structures that requires deeper inspection +func AssertDeepEquals(t *testing.T, expected, actual interface{}) { + pre := prefix(2) + + differed := false + deepDiff(expected, actual, func(path []string, expected, actual interface{}) { + differed = true + t.Errorf("\033[1;31m%sat %s expected %s, but got %s\033[0m", + pre, + strings.Join(path, ""), + green(expected), + yellow(actual)) + }) + if differed { + logFatal(t, "The structures were different.") + } +} + +// CheckDeepEquals is similar to AssertDeepEquals, except with a non-fatal error +func CheckDeepEquals(t *testing.T, expected, actual interface{}) { + pre := prefix(2) + + deepDiff(expected, actual, func(path []string, expected, actual interface{}) { + t.Errorf("\033[1;31m%s at %s expected %s, but got %s\033[0m", + pre, + strings.Join(path, ""), + green(expected), + yellow(actual)) + }) +} + +// isJSONEquals is a utility function that implements JSON comparison for AssertJSONEquals and +// CheckJSONEquals. +func isJSONEquals(t *testing.T, expectedJSON string, actual interface{}) bool { + var parsedExpected, parsedActual interface{} + err := json.Unmarshal([]byte(expectedJSON), &parsedExpected) + if err != nil { + t.Errorf("Unable to parse expected value as JSON: %v", err) + return false + } + + jsonActual, err := json.Marshal(actual) + AssertNoErr(t, err) + err = json.Unmarshal(jsonActual, &parsedActual) + AssertNoErr(t, err) + + if !reflect.DeepEqual(parsedExpected, parsedActual) { + prettyExpected, err := json.MarshalIndent(parsedExpected, "", " ") + if err != nil { + t.Logf("Unable to pretty-print expected JSON: %v\n%s", err, expectedJSON) + } else { + // We can't use green() here because %#v prints prettyExpected as a byte array literal, which + // is... unhelpful. Converting it to a string first leaves "\n" uninterpreted for some reason. + t.Logf("Expected JSON:\n%s%s%s", greenCode, prettyExpected, resetCode) + } + + prettyActual, err := json.MarshalIndent(actual, "", " ") + if err != nil { + t.Logf("Unable to pretty-print actual JSON: %v\n%#v", err, actual) + } else { + // We can't use yellow() for the same reason. + t.Logf("Actual JSON:\n%s%s%s", yellowCode, prettyActual, resetCode) + } + + return false + } + return true +} + +// AssertJSONEquals serializes a value as JSON, parses an expected string as JSON, and ensures that +// both are consistent. If they aren't, the expected and actual structures are pretty-printed and +// shown for comparison. +// +// This is useful for comparing structures that are built as nested map[string]interface{} values, +// which are a pain to construct as literals. +func AssertJSONEquals(t *testing.T, expectedJSON string, actual interface{}) { + if !isJSONEquals(t, expectedJSON, actual) { + logFatal(t, "The generated JSON structure differed.") + } +} + +// CheckJSONEquals is similar to AssertJSONEquals, but nonfatal. +func CheckJSONEquals(t *testing.T, expectedJSON string, actual interface{}) { + if !isJSONEquals(t, expectedJSON, actual) { + logError(t, "The generated JSON structure differed.") + } +} + +// AssertNoErr is a convenience function for checking whether an error value is +// an actual error +func AssertNoErr(t *testing.T, e error) { + if e != nil { + logFatal(t, fmt.Sprintf("unexpected error %s", yellow(e.Error()))) + } +} + +// CheckNoErr is similar to AssertNoErr, except with a non-fatal error +func CheckNoErr(t *testing.T, e error) { + if e != nil { + logError(t, fmt.Sprintf("unexpected error %s", yellow(e.Error()))) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/doc.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/doc.go new file mode 100644 index 000000000000..25b4dfebbbe3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/doc.go @@ -0,0 +1,4 @@ +/* +Package testhelper container methods that are useful for writing unit tests. +*/ +package testhelper diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/http_responses.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/http_responses.go new file mode 100644 index 000000000000..e1f1f9ac0e89 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/testhelper/http_responses.go @@ -0,0 +1,91 @@ +package testhelper + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" +) + +var ( + // Mux is a multiplexer that can be used to register handlers. + Mux *http.ServeMux + + // Server is an in-memory HTTP server for testing. + Server *httptest.Server +) + +// SetupHTTP prepares the Mux and Server. +func SetupHTTP() { + Mux = http.NewServeMux() + Server = httptest.NewServer(Mux) +} + +// TeardownHTTP releases HTTP-related resources. +func TeardownHTTP() { + Server.Close() +} + +// Endpoint returns a fake endpoint that will actually target the Mux. +func Endpoint() string { + return Server.URL + "/" +} + +// TestFormValues ensures that all the URL parameters given to the http.Request are the same as values. +func TestFormValues(t *testing.T, r *http.Request, values map[string]string) { + want := url.Values{} + for k, v := range values { + want.Add(k, v) + } + + r.ParseForm() + if !reflect.DeepEqual(want, r.Form) { + t.Errorf("Request parameters = %v, want %v", r.Form, want) + } +} + +// TestMethod checks that the Request has the expected method (e.g. GET, POST). +func TestMethod(t *testing.T, r *http.Request, expected string) { + if expected != r.Method { + t.Errorf("Request method = %v, expected %v", r.Method, expected) + } +} + +// TestHeader checks that the header on the http.Request matches the expected value. +func TestHeader(t *testing.T, r *http.Request, header string, expected string) { + if actual := r.Header.Get(header); expected != actual { + t.Errorf("Header %s = %s, expected %s", header, actual, expected) + } +} + +// TestBody verifies that the request body matches an expected body. +func TestBody(t *testing.T, r *http.Request, expected string) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Unable to read body: %v", err) + } + str := string(b) + if expected != str { + t.Errorf("Body = %s, expected %s", str, expected) + } +} + +// TestJSONRequest verifies that the JSON payload of a request matches an expected structure, without asserting things about +// whitespace or ordering. +func TestJSONRequest(t *testing.T, r *http.Request, expected string) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Unable to read request body: %v", err) + } + + var actualJSON interface{} + err = json.Unmarshal(b, &actualJSON) + if err != nil { + t.Errorf("Unable to parse request body as JSON: %v", err) + } + + CheckJSONEquals(t, expected, actualJSON) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go new file mode 100644 index 000000000000..fbd9fe9f3813 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util.go @@ -0,0 +1,44 @@ +package gophercloud + +import ( + "errors" + "strings" + "time" +) + +// WaitFor polls a predicate function, once per second, up to a timeout limit. +// 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 { + // Force a 1s sleep + time.Sleep(1 * time.Second) + + // If a timeout is set, and that's been exceeded, shut it down + if timeout >= 0 && time.Now().Second()-start >= timeout { + return errors.New("A timeout occurred") + } + + // Execute the function + satisfied, err := predicate() + if err != nil { + return err + } + if satisfied { + return nil + } + } +} + +// 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 + "/" + } + return url +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/util_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util_test.go new file mode 100644 index 000000000000..5a15a005d35e --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/util_test.go @@ -0,0 +1,14 @@ +package gophercloud + +import ( + "testing" + + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestWaitFor(t *testing.T) { + err := WaitFor(5, func() (bool, error) { + return true, nil + }) + th.CheckNoErr(t, err) +} diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/LICENSE b/Godeps/_workspace/src/github.com/vaughan0/go-ini/LICENSE new file mode 100644 index 000000000000..968b45384d07 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2013 Vaughan Newton + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/README.md b/Godeps/_workspace/src/github.com/vaughan0/go-ini/README.md new file mode 100644 index 000000000000..d5cd4e74b003 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/README.md @@ -0,0 +1,70 @@ +go-ini +====== + +INI parsing library for Go (golang). + +View the API documentation [here](http://godoc.org/github.com/vaughan0/go-ini). + +Usage +----- + +Parse an INI file: + +```go +import "github.com/vaughan0/go-ini" + +file, err := ini.LoadFile("myfile.ini") +``` + +Get data from the parsed file: + +```go +name, ok := file.Get("person", "name") +if !ok { + panic("'name' variable missing from 'person' section") +} +``` + +Iterate through values in a section: + +```go +for key, value := range file["mysection"] { + fmt.Printf("%s => %s\n", key, value) +} +``` + +Iterate through sections in a file: + +```go +for name, section := range file { + fmt.Printf("Section name: %s\n", name) +} +``` + +File Format +----------- + +INI files are parsed by go-ini line-by-line. Each line may be one of the following: + + * A section definition: [section-name] + * A property: key = value + * A comment: #blahblah _or_ ;blahblah + * Blank. The line will be ignored. + +Properties defined before any section headers are placed in the default section, which has +the empty string as it's key. + +Example: + +```ini +# I am a comment +; So am I! + +[apples] +colour = red or green +shape = applish + +[oranges] +shape = square +colour = blue +``` diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini.go b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini.go new file mode 100644 index 000000000000..81aeb32f8b28 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini.go @@ -0,0 +1,123 @@ +// Package ini provides functions for parsing INI configuration files. +package ini + +import ( + "bufio" + "fmt" + "io" + "os" + "regexp" + "strings" +) + +var ( + sectionRegex = regexp.MustCompile(`^\[(.*)\]$`) + assignRegex = regexp.MustCompile(`^([^=]+)=(.*)$`) +) + +// ErrSyntax is returned when there is a syntax error in an INI file. +type ErrSyntax struct { + Line int + Source string // The contents of the erroneous line, without leading or trailing whitespace +} + +func (e ErrSyntax) Error() string { + return fmt.Sprintf("invalid INI syntax on line %d: %s", e.Line, e.Source) +} + +// A File represents a parsed INI file. +type File map[string]Section + +// A Section represents a single section of an INI file. +type Section map[string]string + +// Returns a named Section. A Section will be created if one does not already exist for the given name. +func (f File) Section(name string) Section { + section := f[name] + if section == nil { + section = make(Section) + f[name] = section + } + return section +} + +// Looks up a value for a key in a section and returns that value, along with a boolean result similar to a map lookup. +func (f File) Get(section, key string) (value string, ok bool) { + if s := f[section]; s != nil { + value, ok = s[key] + } + return +} + +// Loads INI data from a reader and stores the data in the File. +func (f File) Load(in io.Reader) (err error) { + bufin, ok := in.(*bufio.Reader) + if !ok { + bufin = bufio.NewReader(in) + } + return parseFile(bufin, f) +} + +// Loads INI data from a named file and stores the data in the File. +func (f File) LoadFile(file string) (err error) { + in, err := os.Open(file) + if err != nil { + return + } + defer in.Close() + return f.Load(in) +} + +func parseFile(in *bufio.Reader, file File) (err error) { + section := "" + lineNum := 0 + for done := false; !done; { + var line string + if line, err = in.ReadString('\n'); err != nil { + if err == io.EOF { + done = true + } else { + return + } + } + lineNum++ + line = strings.TrimSpace(line) + if len(line) == 0 { + // Skip blank lines + continue + } + if line[0] == ';' || line[0] == '#' { + // Skip comments + continue + } + + if groups := assignRegex.FindStringSubmatch(line); groups != nil { + key, val := groups[1], groups[2] + key, val = strings.TrimSpace(key), strings.TrimSpace(val) + file.Section(section)[key] = val + } else if groups := sectionRegex.FindStringSubmatch(line); groups != nil { + name := strings.TrimSpace(groups[1]) + section = name + // Create the section if it does not exist + file.Section(section) + } else { + return ErrSyntax{lineNum, line} + } + + } + return nil +} + +// Loads and returns a File from a reader. +func Load(in io.Reader) (File, error) { + file := make(File) + err := file.Load(in) + return file, err +} + +// Loads and returns an INI File from a file on disk. +func LoadFile(filename string) (File, error) { + file := make(File) + err := file.LoadFile(filename) + return file, err +} diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_linux_test.go b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_linux_test.go new file mode 100644 index 000000000000..38a6f0004cf6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_linux_test.go @@ -0,0 +1,43 @@ +package ini + +import ( + "reflect" + "syscall" + "testing" +) + +func TestLoadFile(t *testing.T) { + originalOpenFiles := numFilesOpen(t) + + file, err := LoadFile("test.ini") + if err != nil { + t.Fatal(err) + } + + if originalOpenFiles != numFilesOpen(t) { + t.Error("test.ini not closed") + } + + if !reflect.DeepEqual(file, File{"default": {"stuff": "things"}}) { + t.Error("file not read correctly") + } +} + +func numFilesOpen(t *testing.T) (num uint64) { + var rlimit syscall.Rlimit + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimit) + if err != nil { + t.Fatal(err) + } + maxFds := int(rlimit.Cur) + + var stat syscall.Stat_t + for i := 0; i < maxFds; i++ { + if syscall.Fstat(i, &stat) == nil { + num++ + } else { + return + } + } + return +} diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_test.go b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_test.go new file mode 100644 index 000000000000..06a4d05eaf08 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_test.go @@ -0,0 +1,89 @@ +package ini + +import ( + "reflect" + "strings" + "testing" +) + +func TestLoad(t *testing.T) { + src := ` + # Comments are ignored + + herp = derp + + [foo] + hello=world + whitespace should = not matter + ; sneaky semicolon-style comment + multiple = equals = signs + + [bar] + this = that` + + file, err := Load(strings.NewReader(src)) + if err != nil { + t.Fatal(err) + } + check := func(section, key, expect string) { + if value, _ := file.Get(section, key); value != expect { + t.Errorf("Get(%q, %q): expected %q, got %q", section, key, expect, value) + } + } + + check("", "herp", "derp") + check("foo", "hello", "world") + check("foo", "whitespace should", "not matter") + check("foo", "multiple", "equals = signs") + check("bar", "this", "that") +} + +func TestSyntaxError(t *testing.T) { + src := ` + # Line 2 + [foo] + bar = baz + # Here's an error on line 6: + wut? + herp = derp` + _, err := Load(strings.NewReader(src)) + t.Logf("%T: %v", err, err) + if err == nil { + t.Fatal("expected an error, got nil") + } + syntaxErr, ok := err.(ErrSyntax) + if !ok { + t.Fatal("expected an error of type ErrSyntax") + } + if syntaxErr.Line != 6 { + t.Fatal("incorrect line number") + } + if syntaxErr.Source != "wut?" { + t.Fatal("incorrect source") + } +} + +func TestDefinedSectionBehaviour(t *testing.T) { + check := func(src string, expect File) { + file, err := Load(strings.NewReader(src)) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(file, expect) { + t.Errorf("expected %v, got %v", expect, file) + } + } + // No sections for an empty file + check("", File{}) + // Default section only if there are actually values for it + check("foo=bar", File{"": {"foo": "bar"}}) + // User-defined sections should always be present, even if empty + check("[a]\n[b]\nfoo=bar", File{ + "a": {}, + "b": {"foo": "bar"}, + }) + check("foo=bar\n[a]\nthis=that", File{ + "": {"foo": "bar"}, + "a": {"this": "that"}, + }) +} diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/test.ini b/Godeps/_workspace/src/github.com/vaughan0/go-ini/test.ini new file mode 100644 index 000000000000..d13c999e254c --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/test.ini @@ -0,0 +1,2 @@ +[default] +stuff = things diff --git a/hack/common.sh b/hack/common.sh index e89d2591894b..943a30f64f70 100755 --- a/hack/common.sh +++ b/hack/common.sh @@ -56,8 +56,18 @@ readonly OPENSHIFT_BINARY_SYMLINKS=( openshift-sti-build openshift-docker-build openshift-gitserver + origin osc + os osadm + oadm + kubectl + kubernetes + kubelet + kube-proxy + kube-apiserver + kube-controller-manager + kube-scheduler ) readonly OPENSHIFT_BINARY_COPY=( osc diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 7286fc705e27..666a421d33be 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -207,6 +207,7 @@ osc get templates echo "templates: ok" # verify some default commands +[ "$(openshift 2>&1)" ] [ "$(openshift cli)" ] [ "$(openshift ex)" ] [ "$(openshift admin config 2>&1)" ] @@ -216,6 +217,14 @@ echo "templates: ok" [ "$(openshift kubectl 2>&1)" ] [ "$(openshift kube 2>&1)" ] [ "$(openshift admin 2>&1)" ] +[ "$(openshift start kubernetes 2>&1)" ] +[ "$(kubernetes 2>&1)" ] +[ "$(kubectl 2>&1)" ] +[ "$(osc 2>&1)" ] +[ "$(os 2>&1)" ] +[ "$(osadm 2>&1)" ] +[ "$(oadm 2>&1)" ] +[ "$(origin 2>&1)" ] # help for root commands must be consistent [ "$(openshift | grep 'OpenShift Application Platform')" ] @@ -226,6 +235,7 @@ echo "templates: ok" [ "$(openshift kubectl 2>&1 | grep 'Kubernetes cluster')" ] [ "$(osadm 2>&1 | grep 'OpenShift Administrative Commands')" ] [ "$(openshift admin 2>&1 | grep 'OpenShift Administrative Commands')" ] +[ "$(openshift start kubernetes 2>&1 | grep 'Kubernetes server components')" ] # help for root commands with --help flag must be consistent [ "$(openshift --help 2>&1 | grep 'OpenShift Application Platform')" ] @@ -390,6 +400,7 @@ echo "edit: ok" osc delete all --all osc new-app https://github.com/openshift/ruby-hello-world -l app=ruby +wait_for_command 'osc get rc/ruby-hello-world-1' "${TIME_MIN}" # resize rc via deployment configuration osc resize dc ruby-hello-world --replicas=1 # resize directly diff --git a/pkg/cmd/admin/project/new_project.go b/pkg/cmd/admin/project/new_project.go index 90724a79c4c6..c1b81365abaa 100644 --- a/pkg/cmd/admin/project/new_project.go +++ b/pkg/cmd/admin/project/new_project.go @@ -31,13 +31,20 @@ type NewProjectOptions struct { AdminUser string } +const newProjectLong = `Create a new project + +Use this command to create a project. You may optionally specify metadata about the project, +an admin user (and role, if you want to use a non-default admin role), and a node selector +to restrict which nodes pods in this project can be scheduled to. +` + func NewCmdNewProject(name, fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command { options := &NewProjectOptions{} cmd := &cobra.Command{ Use: name + " NAME [--display-name=DISPLAYNAME] [--description=DESCRIPTION]", - Short: "create a new project", - Long: `create a new project`, + Short: "Create a new project", + Long: newProjectLong, Run: func(cmd *cobra.Command, args []string) { if err := options.complete(args); err != nil { kcmdutil.CheckErr(kcmdutil.UsageError(cmd, err.Error())) diff --git a/pkg/cmd/cli/cli.go b/pkg/cmd/cli/cli.go index e2a5016ac571..cd81eab25fdd 100644 --- a/pkg/cmd/cli/cli.go +++ b/pkg/cmd/cli/cli.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "io" "os" "github.com/golang/glog" @@ -97,11 +98,10 @@ func NewCommandCLI(name, fullName string) *cobra.Command { // NewCmdKubectl provides exactly the functionality from Kubernetes, // but with support for OpenShift resources -func NewCmdKubectl(name string) *cobra.Command { +func NewCmdKubectl(name string, out io.Writer) *cobra.Command { flags := pflag.NewFlagSet("", pflag.ContinueOnError) f := clientcmd.New(flags) - out := os.Stdout - cmds := kubecmd.NewKubectlCommand(f.Factory, os.Stdin, os.Stdout, os.Stderr) + cmds := kubecmd.NewKubectlCommand(f.Factory, os.Stdin, out, os.Stderr) cmds.Aliases = []string{"kubectl"} cmds.Use = name cmds.Short = "Kubernetes cluster management via kubectl" diff --git a/pkg/cmd/experimental/ipfailover/ipfailover.go b/pkg/cmd/experimental/ipfailover/ipfailover.go index 581dbd10a91c..86344dfb53be 100644 --- a/pkg/cmd/experimental/ipfailover/ipfailover.go +++ b/pkg/cmd/experimental/ipfailover/ipfailover.go @@ -47,7 +47,7 @@ ALPHA: This command is currently being actively developed. It is intended $ %[1]s %[2]s ipfailover --selector="router=us-west-ha" --virtual-ips="1.2.3.4,10.1.1.100-104,5.6.7.8" --watch-port=80 --replicas=4 --create // Use a different IP failover config image and see the configuration: - $ %[1]s %[2]s ipf-alt --selector="jack=the-vipper" --virtual-ips="1.2.3.4" -o yaml --images=myrepo/myipfailover:mytag` + $ %[1]s %[2]s ipf-alt --selector="hagroup=us-west-ha" --virtual-ips="1.2.3.4" -o yaml --images=myrepo/myipfailover:mytag` ) func NewCmdIPFailoverConfig(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command { @@ -62,7 +62,7 @@ func NewCmdIPFailoverConfig(f *clientcmd.Factory, parentName, name string, out i cmd := &cobra.Command{ Use: fmt.Sprintf("%s [NAME]", name), - Short: "Configure or view IP Failover configuration", + Short: "Install an IP failover group to a set of nodes", Long: ipFailover_long, Example: fmt.Sprintf(ipFailover_example, parentName, name), Run: func(cmd *cobra.Command, args []string) { diff --git a/pkg/cmd/experimental/registry/registry.go b/pkg/cmd/experimental/registry/registry.go index 4c7b278ed1e3..a845b2f08a0a 100644 --- a/pkg/cmd/experimental/registry/registry.go +++ b/pkg/cmd/experimental/registry/registry.go @@ -84,7 +84,7 @@ func NewCmdRegistry(f *clientcmd.Factory, parentName, name string, out io.Writer cmd := &cobra.Command{ Use: name, - Short: "Install and check OpenShift Docker registry", + Short: "Install the OpenShift Docker registry", Long: registry_long, Example: fmt.Sprintf(registry_example, parentName, name), Run: func(cmd *cobra.Command, args []string) { diff --git a/pkg/cmd/experimental/router/router.go b/pkg/cmd/experimental/router/router.go index 8759b0db7967..de8687520999 100644 --- a/pkg/cmd/experimental/router/router.go +++ b/pkg/cmd/experimental/router/router.go @@ -80,7 +80,7 @@ func NewCmdRouter(f *clientcmd.Factory, parentName, name string, out io.Writer) cmd := &cobra.Command{ Use: fmt.Sprintf("%s [NAME]", name), - Short: "Install and check OpenShift routers", + Short: "Install an OpenShift router", Long: router_long, Example: fmt.Sprintf(router_example, parentName, name), Run: func(cmd *cobra.Command, args []string) { diff --git a/pkg/cmd/openshift/openshift.go b/pkg/cmd/openshift/openshift.go index 5d41884952f2..97a1f567dc8b 100644 --- a/pkg/cmd/openshift/openshift.go +++ b/pkg/cmd/openshift/openshift.go @@ -22,6 +22,7 @@ import ( "github.com/openshift/origin/pkg/cmd/infra/gitserver" "github.com/openshift/origin/pkg/cmd/infra/router" "github.com/openshift/origin/pkg/cmd/server/start" + "github.com/openshift/origin/pkg/cmd/server/start/kubernetes" "github.com/openshift/origin/pkg/cmd/templates" "github.com/openshift/origin/pkg/cmd/util/clientcmd" "github.com/openshift/origin/pkg/version" @@ -61,12 +62,28 @@ func CommandFor(basename string) *cobra.Command { cmd = builder.NewCommandDockerBuilder(basename) case "openshift-gitserver": cmd = gitserver.NewCommandGitServer(basename) - case "osc": + case "osc", "os": cmd = cli.NewCommandCLI(basename, basename) - case "osadm": + case "osadm", "oadm": cmd = admin.NewCommandAdmin(basename, basename, os.Stdout) + case "kubectl": + cmd = cli.NewCmdKubectl(basename, os.Stdout) + case "kube-apiserver": + cmd = kubernetes.NewAPIServerCommand(basename, basename, os.Stdout) + case "kube-controller-manager": + cmd = kubernetes.NewControllersCommand(basename, basename, os.Stdout) + case "kubelet": + cmd = kubernetes.NewKubeletCommand(basename, basename, os.Stdout) + case "kube-proxy": + cmd = kubernetes.NewProxyCommand(basename, basename, os.Stdout) + case "kube-scheduler": + cmd = kubernetes.NewSchedulerCommand(basename, basename, os.Stdout) + case "kubernetes": + cmd = kubernetes.NewCommand(basename, basename, os.Stdout) + case "origin": + cmd = NewCommandOpenShift("origin") default: - cmd = NewCommandOpenShift() + cmd = NewCommandOpenShift("openshift") } templates.UseMainTemplates(cmd) @@ -76,9 +93,9 @@ func CommandFor(basename string) *cobra.Command { } // NewCommandOpenShift creates the standard OpenShift command -func NewCommandOpenShift() *cobra.Command { +func NewCommandOpenShift(name string) *cobra.Command { root := &cobra.Command{ - Use: "openshift", + Use: name, Short: "OpenShift helps you build, deploy, and manage your cloud applications", Long: openshift_long, Run: func(c *cobra.Command, args []string) { @@ -87,13 +104,13 @@ func NewCommandOpenShift() *cobra.Command { }, } - startAllInOne, _ := start.NewCommandStartAllInOne(os.Stdout) + startAllInOne, _ := start.NewCommandStartAllInOne(name, os.Stdout) root.AddCommand(startAllInOne) - root.AddCommand(admin.NewCommandAdmin("admin", "openshift admin", os.Stdout)) - root.AddCommand(cli.NewCommandCLI("cli", "openshift cli")) - root.AddCommand(cli.NewCmdKubectl("kube")) - root.AddCommand(newExperimentalCommand("ex", "openshift ex")) - root.AddCommand(version.NewVersionCommand("openshift")) + root.AddCommand(admin.NewCommandAdmin("admin", name+" admin", os.Stdout)) + root.AddCommand(cli.NewCommandCLI("cli", name+" cli")) + root.AddCommand(cli.NewCmdKubectl("kube", os.Stdout)) + root.AddCommand(newExperimentalCommand("ex", name+" ex")) + root.AddCommand(version.NewVersionCommand(name)) // infra commands are those that are bundled with the binary but not displayed to end users // directly diff --git a/pkg/cmd/server/admin/create_nodeconfig.go b/pkg/cmd/server/admin/create_nodeconfig.go index 4f6b214dae3d..97125901a356 100644 --- a/pkg/cmd/server/admin/create_nodeconfig.go +++ b/pkg/cmd/server/admin/create_nodeconfig.go @@ -58,7 +58,7 @@ func NewCommandNodeConfig(commandName string, fullName string, out io.Writer) *c cmd := &cobra.Command{ Use: commandName, - Short: "Create a portable client folder containing a client certificate, a client key, a server certificate authority, and a .kubeconfig file.", + Short: "Create a configuration bundle for a node", Run: func(cmd *cobra.Command, args []string) { if err := options.Validate(args); err != nil { kcmdutil.CheckErr(kcmdutil.UsageError(cmd, err.Error())) diff --git a/pkg/cmd/server/start/command_test.go b/pkg/cmd/server/start/command_test.go index 5008b3c589de..949bb4af0068 100644 --- a/pkg/cmd/server/start/command_test.go +++ b/pkg/cmd/server/start/command_test.go @@ -249,7 +249,7 @@ func executeAllInOneCommandWithConfigs(args []string) (*MasterArgs, *configapi.M }, } - openshiftStartCommand, cfg := NewCommandStartAllInOne(os.Stdout) + openshiftStartCommand, cfg := NewCommandStartAllInOne("openshift start", os.Stdout) root.AddCommand(openshiftStartCommand) root.SetArgs(argsToUse) root.Execute() diff --git a/pkg/cmd/server/start/kubernetes/apiserver.go b/pkg/cmd/server/start/kubernetes/apiserver.go new file mode 100644 index 000000000000..aa7018430d76 --- /dev/null +++ b/pkg/cmd/server/start/kubernetes/apiserver.go @@ -0,0 +1,47 @@ +package kubernetes + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/GoogleCloudPlatform/kubernetes/cmd/kube-apiserver/app" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +const apiserverLong = `Start Kubernetes apiserver + +This command launches an instance of the Kubernetes apiserver (kube-apiserver).` + +// NewAPIServerCommand provides a CLI handler for the 'apiserver' command +func NewAPIServerCommand(name, fullName string, out io.Writer) *cobra.Command { + s := app.NewAPIServer() + + cmd := &cobra.Command{ + Use: name, + Short: "Launch Kubernetes apiserver (kube-apiserver)", + Long: apiserverLong, + Run: func(c *cobra.Command, args []string) { + startProfiler() + + util.InitLogs() + defer util.FlushLogs() + + if err := s.Run(pflag.CommandLine.Args()); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + }, + } + cmd.SetOutput(out) + + flags := cmd.Flags() + //TODO: uncomment after picking up a newer cobra + //pflag.AddFlagSetToPFlagSet(flag, flags) + s.AddFlags(flags) + + return cmd +} diff --git a/pkg/cmd/server/start/kubernetes/controllers.go b/pkg/cmd/server/start/kubernetes/controllers.go new file mode 100644 index 000000000000..a1d359737f6f --- /dev/null +++ b/pkg/cmd/server/start/kubernetes/controllers.go @@ -0,0 +1,47 @@ +package kubernetes + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/GoogleCloudPlatform/kubernetes/cmd/kube-controller-manager/app" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +const controllersLong = `Start Kubernetes controller manager + +This command launches an instance of the Kubernetes controller-manager (kube-controller-manager).` + +// NewControllersCommand provides a CLI handler for the 'controller-manager' command +func NewControllersCommand(name, fullName string, out io.Writer) *cobra.Command { + s := app.NewCMServer() + + cmd := &cobra.Command{ + Use: name, + Short: "Launch Kubernetes controller manager (kube-controller-manager)", + Long: controllersLong, + Run: func(c *cobra.Command, args []string) { + startProfiler() + + util.InitLogs() + defer util.FlushLogs() + + if err := s.Run(pflag.CommandLine.Args()); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + }, + } + cmd.SetOutput(out) + + flags := cmd.Flags() + //TODO: uncomment after picking up a newer cobra + //pflag.AddFlagSetToPFlagSet(flag, flags) + s.AddFlags(flags) + + return cmd +} diff --git a/pkg/cmd/server/start/kubernetes/kubelet.go b/pkg/cmd/server/start/kubernetes/kubelet.go new file mode 100644 index 000000000000..1bf04bcbb7df --- /dev/null +++ b/pkg/cmd/server/start/kubernetes/kubelet.go @@ -0,0 +1,48 @@ +package kubernetes + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/GoogleCloudPlatform/kubernetes/cmd/kubelet/app" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +const kubeletLog = `Start Kubelet + +This command launches a Kubelet. All options are exposed. Use 'openshift start node' for +starting from a configuration file.` + +// NewKubeletCommand provides a CLI handler for the 'kubelet' command +func NewKubeletCommand(name, fullName string, out io.Writer) *cobra.Command { + s := app.NewKubeletServer() + + cmd := &cobra.Command{ + Use: name, + Short: "Launch the Kubelet (kubelet)", + Long: kubeletLog, + Run: func(c *cobra.Command, args []string) { + startProfiler() + + util.InitLogs() + defer util.FlushLogs() + + if err := s.Run(pflag.CommandLine.Args()); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + }, + } + cmd.SetOutput(out) + + flags := cmd.Flags() + //TODO: uncomment after picking up a newer cobra + //pflag.AddFlagSetToPFlagSet(flag, flags) + s.AddFlags(flags) + + return cmd +} diff --git a/pkg/cmd/server/start/kubernetes/kubernetes.go b/pkg/cmd/server/start/kubernetes/kubernetes.go new file mode 100644 index 000000000000..70f7cee5bc65 --- /dev/null +++ b/pkg/cmd/server/start/kubernetes/kubernetes.go @@ -0,0 +1,52 @@ +package kubernetes + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/golang/glog" + "github.com/spf13/cobra" + + cmdutil "github.com/openshift/origin/pkg/cmd/util" + "github.com/openshift/origin/pkg/version" +) + +const kubernetesLong = `Start Kubernetes server components + +The primary Kubernetes server components can be started individually using their direct +arguments. No configuration settings will be used when launching these components. +` + +func NewCommand(name, fullName string, out io.Writer) *cobra.Command { + cmds := &cobra.Command{ + Use: name, + Short: "Kubernetes server components", + Long: fmt.Sprintf(kubernetesLong), + Run: func(c *cobra.Command, args []string) { + c.SetOutput(os.Stdout) + c.Help() + }, + } + + cmds.AddCommand(NewAPIServerCommand("apiserver", fullName+" apiserver", out)) + cmds.AddCommand(NewControllersCommand("controller-manager", fullName+" controller-manager", out)) + cmds.AddCommand(NewKubeletCommand("kubelet", fullName+" kubelet", out)) + cmds.AddCommand(NewProxyCommand("proxy", fullName+" proxy", out)) + cmds.AddCommand(NewSchedulerCommand("scheduler", fullName+" scheduler", out)) + if "hyperkube" == fullName { + cmds.AddCommand(version.NewVersionCommand(fullName)) + } + + return cmds +} + +func startProfiler() { + if cmdutil.Env("OPENSHIFT_PROFILE", "") == "web" { + go func() { + glog.Infof("Starting profiling endpoint at http://127.0.0.1:6060/debug/pprof/") + glog.Fatal(http.ListenAndServe("127.0.0.1:6060", nil)) + }() + } +} diff --git a/pkg/cmd/server/start/kubernetes/proxy.go b/pkg/cmd/server/start/kubernetes/proxy.go new file mode 100644 index 000000000000..26cb450b6ba4 --- /dev/null +++ b/pkg/cmd/server/start/kubernetes/proxy.go @@ -0,0 +1,47 @@ +package kubernetes + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/GoogleCloudPlatform/kubernetes/cmd/kube-proxy/app" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +const proxyLong = `Start Kubernetes Proxy + +This command launches an instance of the Kubernetes proxy (kube-proxy).` + +// NewProxyCommand provides a CLI handler for the 'proxy' command +func NewProxyCommand(name, fullName string, out io.Writer) *cobra.Command { + s := app.NewProxyServer() + + cmd := &cobra.Command{ + Use: name, + Short: "Launch Kubernetes proxy (kube-proxy)", + Long: proxyLong, + Run: func(c *cobra.Command, args []string) { + startProfiler() + + util.InitLogs() + defer util.FlushLogs() + + if err := s.Run(pflag.CommandLine.Args()); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + }, + } + cmd.SetOutput(out) + + flags := cmd.Flags() + //TODO: uncomment after picking up a newer cobra + //pflag.AddFlagSetToPFlagSet(flag, flags) + s.AddFlags(flags) + + return cmd +} diff --git a/pkg/cmd/server/start/kubernetes/scheduler.go b/pkg/cmd/server/start/kubernetes/scheduler.go new file mode 100644 index 000000000000..f1fc568f61d6 --- /dev/null +++ b/pkg/cmd/server/start/kubernetes/scheduler.go @@ -0,0 +1,47 @@ +package kubernetes + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/plugin/cmd/kube-scheduler/app" +) + +const schedulerLong = `Start Kubernetes scheduler + +This command launches an instance of the Kubernetes controller-manager (kube-controller-manager).` + +// NewSchedulerCommand provides a CLI handler for the 'scheduler' command +func NewSchedulerCommand(name, fullName string, out io.Writer) *cobra.Command { + s := app.NewSchedulerServer() + + cmd := &cobra.Command{ + Use: name, + Short: "Launch Kubernetes scheduler (kube-scheduler)", + Long: controllersLong, + Run: func(c *cobra.Command, args []string) { + startProfiler() + + util.InitLogs() + defer util.FlushLogs() + + if err := s.Run(pflag.CommandLine.Args()); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + }, + } + cmd.SetOutput(out) + + flags := cmd.Flags() + //TODO: uncomment after picking up a newer cobra + //pflag.AddFlagSetToPFlagSet(flag, flags) + s.AddFlags(flags) + + return cmd +} diff --git a/pkg/cmd/server/start/start_allinone.go b/pkg/cmd/server/start/start_allinone.go index 10b77f9f9e0c..c4853d9ceab3 100644 --- a/pkg/cmd/server/start/start_allinone.go +++ b/pkg/cmd/server/start/start_allinone.go @@ -20,6 +20,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/openshift/origin/pkg/cmd/server/admin" + "github.com/openshift/origin/pkg/cmd/server/start/kubernetes" cmdutil "github.com/openshift/origin/pkg/cmd/util" ) @@ -56,7 +57,7 @@ You may also pass --etcd=
to connect to an external etcd server. You may also pass --kubeconfig= to connect to an external Kubernetes cluster.` // NewCommandStartMaster provides a CLI handler for 'start' command -func NewCommandStartAllInOne(out io.Writer) (*cobra.Command, *AllInOneOptions) { +func NewCommandStartAllInOne(fullName string, out io.Writer) (*cobra.Command, *AllInOneOptions) { options := &AllInOneOptions{Output: cmdutil.Output{out}} cmd := &cobra.Command{ @@ -113,6 +114,9 @@ func NewCommandStartAllInOne(out io.Writer) (*cobra.Command, *AllInOneOptions) { cmd.AddCommand(startMaster) cmd.AddCommand(startNode) + startKube := kubernetes.NewCommand("kubernetes", fullName, out) + cmd.AddCommand(startKube) + return cmd, options }